@quarri/claude-data-tools 1.2.6 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quarri/claude-data-tools",
3
- "version": "1.2.6",
3
+ "version": "1.3.0",
4
4
  "description": "Quarri Data Assistant - Natural language data analysis with Quarri",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,12 +1,14 @@
1
1
  ---
2
- description: Generate interactive Plotly charts rendered as MCP UI resources
2
+ description: Generate interactive Plotly charts as HTML files
3
3
  globs:
4
4
  alwaysApply: false
5
5
  ---
6
6
 
7
7
  # /quarri-chart - Interactive Chart Generation
8
8
 
9
- Generate data visualizations as interactive Plotly charts using MCP UI resources.
9
+ Generate data visualizations as interactive **Plotly.js** HTML files that open in the browser.
10
+
11
+ **IMPORTANT**: Always generate HTML files with Plotly.js from CDN. Do NOT generate React components, inline JSON, or any other format.
10
12
 
11
13
  ## When to Use
12
14
 
@@ -16,41 +18,445 @@ Use `/quarri-chart` when users want visualizations:
16
18
  - "Show me a graph of this data"
17
19
  - "Chart sales by category"
18
20
 
21
+ ## Data Size Limits
22
+
23
+ **CRITICAL**: Charts should contain at most **500 data points**. Large datasets must be handled appropriately:
24
+
25
+ | Scenario | Solution |
26
+ |----------|----------|
27
+ | Time series with many dates | Aggregate to appropriate granularity (day→week→month→quarter) |
28
+ | Too many categories | Show Top N (10-20) + "Other" bucket |
29
+ | Scatter plot with many points | Sample data or use density/heatmap |
30
+ | Raw transactional data | Always GROUP BY before charting - never chart individual transactions |
31
+
32
+ ### Example: Handling Large Categorical Data
33
+
34
+ ```sql
35
+ -- BAD: Returns potentially thousands of products
36
+ SELECT product_name, SUM(sales) FROM quarri.schema GROUP BY product_name
37
+
38
+ -- GOOD: Top 10 + Other
39
+ WITH ranked AS (
40
+ SELECT product_name, SUM(sales) as total_sales,
41
+ ROW_NUMBER() OVER (ORDER BY SUM(sales) DESC) as rn
42
+ FROM quarri.schema
43
+ GROUP BY product_name
44
+ )
45
+ SELECT
46
+ CASE WHEN rn <= 10 THEN product_name ELSE 'Other' END as product_name,
47
+ SUM(total_sales) as total_sales
48
+ FROM ranked
49
+ GROUP BY CASE WHEN rn <= 10 THEN product_name ELSE 'Other' END
50
+ ORDER BY total_sales DESC
51
+ ```
52
+
53
+ ### Example: Handling Long Time Series
54
+
55
+ ```sql
56
+ -- BAD: Daily data for 5 years = 1800+ points
57
+ SELECT order_date, SUM(sales) FROM quarri.schema GROUP BY order_date
58
+
59
+ -- GOOD: Aggregate to months
60
+ SELECT DATE_TRUNC('month', order_date) as month, SUM(sales) as total_sales
61
+ FROM quarri.schema
62
+ GROUP BY DATE_TRUNC('month', order_date)
63
+ ORDER BY month
64
+ ```
65
+
19
66
  ## Primary Workflow
20
67
 
21
- ### Step 1: Query the data
68
+ ### Step 1: Query the data (with appropriate aggregation)
69
+
70
+ Use `quarri_execute_sql` to get the data:
71
+
22
72
  ```sql
23
73
  SELECT category, SUM(sales) as total_sales
24
- FROM orders
74
+ FROM quarri.schema
25
75
  GROUP BY category
26
76
  ORDER BY total_sales DESC
77
+ LIMIT 20
27
78
  ```
28
79
 
29
- ### Step 2: Return chart as MCP UI resource
30
-
31
- After getting the data, construct a Plotly configuration and include it in your response. The chart will be rendered as an interactive UI component.
32
-
33
- **Example Plotly config for bar chart:**
34
- ```json
35
- {
36
- "data": [{
37
- "x": ["Category A", "Category B", "Category C"],
38
- "y": [450000, 380000, 290000],
39
- "type": "bar",
40
- "marker": { "color": "#4F46E5" }
41
- }],
42
- "layout": {
43
- "title": "Sales by Category",
44
- "xaxis": { "title": "Category" },
45
- "yaxis": { "title": "Sales ($)", "tickformat": "$,.0f" }
46
- }
47
- }
80
+ ### Step 2: Generate HTML file with Plotly
81
+
82
+ Write an HTML file that:
83
+ 1. Loads Plotly.js from CDN
84
+ 2. Embeds the query results as a JavaScript data array
85
+ 3. Builds Plotly traces from the data
86
+ 4. Renders the chart
87
+
88
+ ### Step 3: Open in browser
89
+
90
+ Use `open <filepath>` (macOS) or `xdg-open <filepath>` (Linux) to display the chart.
91
+
92
+ ## HTML Template
93
+
94
+ Use this template structure for all charts:
95
+
96
+ ```html
97
+ <!DOCTYPE html>
98
+ <html lang="en">
99
+ <head>
100
+ <meta charset="UTF-8">
101
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
102
+ <title>CHART_TITLE</title>
103
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
104
+ <style>
105
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
106
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
107
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <h1>CHART_TITLE</h1>
112
+ <div id="chart"></div>
113
+ <script>
114
+ // Data from query results
115
+ const data = DATA_ARRAY;
116
+
117
+ // Build Plotly traces
118
+ const traces = [TRACE_CONFIG];
119
+
120
+ // Layout configuration
121
+ const layout = LAYOUT_CONFIG;
122
+
123
+ // Render chart
124
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
125
+ </script>
126
+ </body>
127
+ </html>
48
128
  ```
49
129
 
50
- The response should include this as a resource block with:
51
- - URI: `ui://quarri/chart`
52
- - MIME type: `application/vnd.quarri.chart+json`
53
- - Content: JSON with `type: "chart"` and `plotly: { data, layout }`
130
+ ## Complete Examples
131
+
132
+ ### Bar Chart Example
133
+
134
+ **Query:**
135
+ ```sql
136
+ SELECT category, SUM(sales) as total_sales
137
+ FROM quarri.schema
138
+ GROUP BY category
139
+ ORDER BY total_sales DESC
140
+ ```
141
+
142
+ **HTML file to write:**
143
+ ```html
144
+ <!DOCTYPE html>
145
+ <html lang="en">
146
+ <head>
147
+ <meta charset="UTF-8">
148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
149
+ <title>Sales by Category</title>
150
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
151
+ <style>
152
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
153
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
154
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
155
+ </style>
156
+ </head>
157
+ <body>
158
+ <h1>Sales by Category</h1>
159
+ <div id="chart"></div>
160
+ <script>
161
+ const data = [
162
+ { category: "Technology", total_sales: 836154.03 },
163
+ { category: "Furniture", total_sales: 741999.80 },
164
+ { category: "Office Supplies", total_sales: 719047.03 }
165
+ ];
166
+
167
+ const traces = [{
168
+ x: data.map(d => d.category),
169
+ y: data.map(d => d.total_sales),
170
+ type: 'bar',
171
+ marker: { color: '#4F46E5' },
172
+ text: data.map(d => '$' + (d.total_sales / 1000).toFixed(0) + 'K'),
173
+ textposition: 'auto'
174
+ }];
175
+
176
+ const layout = {
177
+ xaxis: { title: 'Category' },
178
+ yaxis: { title: 'Sales ($)', tickformat: '$,.0f' },
179
+ margin: { t: 20, r: 20, b: 60, l: 80 }
180
+ };
181
+
182
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
183
+ </script>
184
+ </body>
185
+ </html>
186
+ ```
187
+
188
+ ### Line Chart Example (Time Series)
189
+
190
+ **Query:**
191
+ ```sql
192
+ SELECT DATE_TRUNC('month', order_date) as month, SUM(sales) as total_sales
193
+ FROM quarri.schema
194
+ GROUP BY DATE_TRUNC('month', order_date)
195
+ ORDER BY month
196
+ ```
197
+
198
+ **HTML file to write:**
199
+ ```html
200
+ <!DOCTYPE html>
201
+ <html lang="en">
202
+ <head>
203
+ <meta charset="UTF-8">
204
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
205
+ <title>Monthly Sales Trend</title>
206
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
207
+ <style>
208
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
209
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
210
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
211
+ </style>
212
+ </head>
213
+ <body>
214
+ <h1>Monthly Sales Trend</h1>
215
+ <div id="chart"></div>
216
+ <script>
217
+ const data = [
218
+ { month: "2021-01-01", total_sales: 94925.57 },
219
+ { month: "2021-02-01", total_sales: 59751.25 },
220
+ { month: "2021-03-01", total_sales: 115973.79 }
221
+ // ... more data points
222
+ ];
223
+
224
+ const traces = [{
225
+ x: data.map(d => d.month),
226
+ y: data.map(d => d.total_sales),
227
+ type: 'scatter',
228
+ mode: 'lines+markers',
229
+ line: { color: '#4F46E5', width: 3 },
230
+ marker: { size: 8 }
231
+ }];
232
+
233
+ const layout = {
234
+ xaxis: { title: 'Month' },
235
+ yaxis: { title: 'Sales ($)', tickformat: '$,.0f' },
236
+ margin: { t: 20, r: 20, b: 60, l: 80 }
237
+ };
238
+
239
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
240
+ </script>
241
+ </body>
242
+ </html>
243
+ ```
244
+
245
+ ### Multi-Series Line Chart
246
+
247
+ **Query:**
248
+ ```sql
249
+ SELECT DATE_TRUNC('month', order_date) as month, category, SUM(sales) as total_sales
250
+ FROM quarri.schema
251
+ GROUP BY DATE_TRUNC('month', order_date), category
252
+ ORDER BY month, category
253
+ ```
254
+
255
+ **HTML file to write:**
256
+ ```html
257
+ <!DOCTYPE html>
258
+ <html lang="en">
259
+ <head>
260
+ <meta charset="UTF-8">
261
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
262
+ <title>Sales Trend by Category</title>
263
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
264
+ <style>
265
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
266
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
267
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
268
+ </style>
269
+ </head>
270
+ <body>
271
+ <h1>Sales Trend by Category</h1>
272
+ <div id="chart"></div>
273
+ <script>
274
+ const data = [
275
+ { month: "2021-01-01", category: "Furniture", total_sales: 28262.03 },
276
+ { month: "2021-01-01", category: "Office Supplies", total_sales: 21879.28 },
277
+ { month: "2021-01-01", category: "Technology", total_sales: 44784.26 }
278
+ // ... more data points
279
+ ];
280
+
281
+ // Group by category
282
+ const categories = [...new Set(data.map(d => d.category))];
283
+ const colors = ['#4F46E5', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
284
+
285
+ const traces = categories.map((cat, i) => {
286
+ const catData = data.filter(d => d.category === cat);
287
+ return {
288
+ x: catData.map(d => d.month),
289
+ y: catData.map(d => d.total_sales),
290
+ name: cat,
291
+ type: 'scatter',
292
+ mode: 'lines+markers',
293
+ line: { color: colors[i % colors.length], width: 2 }
294
+ };
295
+ });
296
+
297
+ const layout = {
298
+ xaxis: { title: 'Month' },
299
+ yaxis: { title: 'Sales ($)', tickformat: '$,.0f' },
300
+ margin: { t: 20, r: 20, b: 60, l: 80 },
301
+ legend: { orientation: 'h', y: -0.15 }
302
+ };
303
+
304
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
305
+ </script>
306
+ </body>
307
+ </html>
308
+ ```
309
+
310
+ ### Horizontal Bar Chart (Many Categories)
311
+
312
+ **HTML file to write:**
313
+ ```html
314
+ <!DOCTYPE html>
315
+ <html lang="en">
316
+ <head>
317
+ <meta charset="UTF-8">
318
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
319
+ <title>Top 10 Products by Sales</title>
320
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
321
+ <style>
322
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
323
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
324
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
325
+ </style>
326
+ </head>
327
+ <body>
328
+ <h1>Top 10 Products by Sales</h1>
329
+ <div id="chart"></div>
330
+ <script>
331
+ const data = [
332
+ { product_name: "Canon imageCLASS 2200", total_sales: 61599.82 },
333
+ { product_name: "Fellowes PB500", total_sales: 27453.38 }
334
+ // ... more products
335
+ ];
336
+
337
+ // Reverse for horizontal bar (top item at top)
338
+ const sorted = [...data].reverse();
339
+
340
+ const traces = [{
341
+ y: sorted.map(d => d.product_name),
342
+ x: sorted.map(d => d.total_sales),
343
+ type: 'bar',
344
+ orientation: 'h',
345
+ marker: { color: '#4F46E5' }
346
+ }];
347
+
348
+ const layout = {
349
+ xaxis: { title: 'Sales ($)', tickformat: '$,.0f' },
350
+ margin: { t: 20, r: 20, b: 60, l: 200 }
351
+ };
352
+
353
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
354
+ </script>
355
+ </body>
356
+ </html>
357
+ ```
358
+
359
+ ### Pie/Donut Chart
360
+
361
+ **HTML file to write:**
362
+ ```html
363
+ <!DOCTYPE html>
364
+ <html lang="en">
365
+ <head>
366
+ <meta charset="UTF-8">
367
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
368
+ <title>Sales Distribution</title>
369
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
370
+ <style>
371
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
372
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
373
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
374
+ </style>
375
+ </head>
376
+ <body>
377
+ <h1>Sales Distribution by Category</h1>
378
+ <div id="chart"></div>
379
+ <script>
380
+ const data = [
381
+ { category: "Technology", total_sales: 836154.03 },
382
+ { category: "Furniture", total_sales: 741999.80 },
383
+ { category: "Office Supplies", total_sales: 719047.03 }
384
+ ];
385
+
386
+ const traces = [{
387
+ labels: data.map(d => d.category),
388
+ values: data.map(d => d.total_sales),
389
+ type: 'pie',
390
+ hole: 0.4,
391
+ marker: { colors: ['#4F46E5', '#10B981', '#F59E0B'] },
392
+ textinfo: 'label+percent'
393
+ }];
394
+
395
+ const layout = {
396
+ margin: { t: 20, r: 20, b: 20, l: 20 },
397
+ showlegend: true
398
+ };
399
+
400
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
401
+ </script>
402
+ </body>
403
+ </html>
404
+ ```
405
+
406
+ ### Grouped Bar Chart
407
+
408
+ **HTML file to write:**
409
+ ```html
410
+ <!DOCTYPE html>
411
+ <html lang="en">
412
+ <head>
413
+ <meta charset="UTF-8">
414
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
415
+ <title>Year over Year Comparison</title>
416
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
417
+ <style>
418
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f8fafc; }
419
+ #chart { width: 100%; height: 600px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
420
+ h1 { color: #1e293b; font-size: 1.5rem; margin-bottom: 16px; }
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <h1>Year over Year Comparison</h1>
425
+ <div id="chart"></div>
426
+ <script>
427
+ const data = [
428
+ { year: 2023, quarter: "Q1", sales: 120000 },
429
+ { year: 2023, quarter: "Q2", sales: 150000 },
430
+ { year: 2024, quarter: "Q1", sales: 140000 },
431
+ { year: 2024, quarter: "Q2", sales: 165000 }
432
+ ];
433
+
434
+ const years = [...new Set(data.map(d => d.year))];
435
+ const colors = ['#4F46E5', '#10B981'];
436
+
437
+ const traces = years.map((year, i) => {
438
+ const yearData = data.filter(d => d.year === year);
439
+ return {
440
+ x: yearData.map(d => d.quarter),
441
+ y: yearData.map(d => d.sales),
442
+ name: String(year),
443
+ type: 'bar',
444
+ marker: { color: colors[i] }
445
+ };
446
+ });
447
+
448
+ const layout = {
449
+ barmode: 'group',
450
+ xaxis: { title: 'Quarter' },
451
+ yaxis: { title: 'Sales ($)', tickformat: '$,.0f' },
452
+ margin: { t: 20, r: 20, b: 60, l: 80 }
453
+ };
454
+
455
+ Plotly.newPlot('chart', traces, layout, { responsive: true });
456
+ </script>
457
+ </body>
458
+ </html>
459
+ ```
54
460
 
55
461
  ## Chart Type Selection
56
462
 
@@ -81,158 +487,6 @@ START: What is the primary analysis goal?
81
487
  └─→ Two numeric → SCATTER PLOT
82
488
  ```
83
489
 
84
- ## Plotly Templates
85
-
86
- ### Bar Chart
87
- ```javascript
88
- {
89
- "data": [{
90
- "x": ["Category A", "Category B", "Category C"],
91
- "y": [450000, 380000, 290000],
92
- "type": "bar",
93
- "marker": {
94
- "color": ["#4F46E5", "#7C3AED", "#A78BFA"]
95
- },
96
- "text": ["$450K", "$380K", "$290K"],
97
- "textposition": "auto"
98
- }],
99
- "layout": {
100
- "title": "Sales by Category",
101
- "xaxis": { "title": "Category" },
102
- "yaxis": { "title": "Sales ($)", "tickformat": "$,.0f" }
103
- }
104
- }
105
- ```
106
-
107
- ### Horizontal Bar (many categories)
108
- ```javascript
109
- {
110
- "data": [{
111
- "y": ["Product A", "Product B", "Product C", "Product D", "Product E"],
112
- "x": [85000, 72000, 65000, 58000, 45000],
113
- "type": "bar",
114
- "orientation": "h",
115
- "marker": { "color": "#4F46E5" }
116
- }],
117
- "layout": {
118
- "title": "Top Products by Revenue",
119
- "xaxis": { "title": "Revenue ($)", "tickformat": "$,.0f" },
120
- "margin": { "l": 120 }
121
- }
122
- }
123
- ```
124
-
125
- ### Line Chart (time series)
126
- ```javascript
127
- {
128
- "data": [{
129
- "x": ["2024-01", "2024-02", "2024-03", "2024-04", "2024-05", "2024-06"],
130
- "y": [120000, 135000, 128000, 145000, 160000, 175000],
131
- "type": "scatter",
132
- "mode": "lines+markers",
133
- "line": { "color": "#4F46E5", "width": 3 },
134
- "marker": { "size": 8 }
135
- }],
136
- "layout": {
137
- "title": "Monthly Revenue Trend",
138
- "xaxis": { "title": "Month" },
139
- "yaxis": { "title": "Revenue ($)", "tickformat": "$,.0f" }
140
- }
141
- }
142
- ```
143
-
144
- ### Multi-Line Chart
145
- ```javascript
146
- {
147
- "data": [
148
- {
149
- "x": ["Jan", "Feb", "Mar", "Apr"],
150
- "y": [100, 120, 115, 140],
151
- "name": "Product A",
152
- "type": "scatter",
153
- "mode": "lines+markers"
154
- },
155
- {
156
- "x": ["Jan", "Feb", "Mar", "Apr"],
157
- "y": [80, 95, 110, 120],
158
- "name": "Product B",
159
- "type": "scatter",
160
- "mode": "lines+markers"
161
- }
162
- ],
163
- "layout": {
164
- "title": "Product Comparison",
165
- "xaxis": { "title": "Month" },
166
- "yaxis": { "title": "Sales" }
167
- }
168
- }
169
- ```
170
-
171
- ### Pie/Donut Chart
172
- ```javascript
173
- {
174
- "data": [{
175
- "labels": ["Technology", "Furniture", "Office Supplies"],
176
- "values": [5471124, 4730801, 4144724],
177
- "type": "pie",
178
- "hole": 0.4,
179
- "marker": {
180
- "colors": ["#4F46E5", "#10B981", "#F59E0B"]
181
- },
182
- "textinfo": "label+percent"
183
- }],
184
- "layout": {
185
- "title": "Sales Distribution by Category"
186
- }
187
- }
188
- ```
189
-
190
- ### Scatter Plot
191
- ```javascript
192
- {
193
- "data": [{
194
- "x": [/* x values */],
195
- "y": [/* y values */],
196
- "mode": "markers",
197
- "type": "scatter",
198
- "marker": {
199
- "size": 10,
200
- "color": "#4F46E5",
201
- "opacity": 0.7
202
- }
203
- }],
204
- "layout": {
205
- "title": "Correlation Analysis",
206
- "xaxis": { "title": "Variable X" },
207
- "yaxis": { "title": "Variable Y" }
208
- }
209
- }
210
- ```
211
-
212
- ### Grouped Bar Chart
213
- ```javascript
214
- {
215
- "data": [
216
- {
217
- "x": ["Q1", "Q2", "Q3", "Q4"],
218
- "y": [120, 150, 180, 200],
219
- "name": "2023",
220
- "type": "bar"
221
- },
222
- {
223
- "x": ["Q1", "Q2", "Q3", "Q4"],
224
- "y": [140, 165, 195, 220],
225
- "name": "2024",
226
- "type": "bar"
227
- }
228
- ],
229
- "layout": {
230
- "title": "Year over Year Comparison",
231
- "barmode": "group"
232
- }
233
- }
234
- ```
235
-
236
490
  ## Color Palettes
237
491
 
238
492
  ```javascript
@@ -265,41 +519,36 @@ tickformat: '.1%' // 45.2%
265
519
  tickformat: '~s' // 1.2M, 3.4K
266
520
  ```
267
521
 
268
- ## MCP UI Resource Format
269
-
270
- The chart should be returned as an MCP UI resource with the following structure:
522
+ ## File Output
271
523
 
272
- ```json
273
- {
274
- "type": "chart",
275
- "title": "Chart Title",
276
- "plotly": {
277
- "data": [/* Plotly data traces */],
278
- "layout": {/* Plotly layout config */}
279
- }
280
- }
524
+ **Always write to a descriptive filename:**
525
+ ```
526
+ /tmp/quarri-chart-sales-by-category.html
527
+ /tmp/quarri-chart-monthly-trend.html
528
+ /tmp/quarri-chart-top-products.html
281
529
  ```
282
530
 
283
- This is automatically rendered by Claude Code when you include the resource block in your response.
284
-
285
- ## Alternative Outputs
531
+ **Then open in browser:**
532
+ ```bash
533
+ # macOS
534
+ open /tmp/quarri-chart-sales-by-category.html
286
535
 
287
- ### QuickChart URL (for sharing/embedding)
288
- When user specifically needs a URL:
289
- ```
290
- https://quickchart.io/chart?c={type:'bar',data:{labels:['A','B','C'],datasets:[{data:[10,20,15]}]}}
536
+ # Linux
537
+ xdg-open /tmp/quarri-chart-sales-by-category.html
291
538
  ```
292
539
 
293
- ### ASCII Chart (terminal only)
294
- When user is in a terminal-only environment:
295
- ```
296
- Sales by Category
540
+ ## Validation Checklist
297
541
 
298
- Technology |████████████████████████████████████████ $5.47M
299
- Furniture |██████████████████████████████████ $4.73M
300
- Office Supplies |██████████████████████████████ $4.14M
301
- 0 $3M $6M
302
- ```
542
+ Before generating a chart, verify:
543
+
544
+ 1. **Format**: Generating an HTML file with Plotly.js from CDN
545
+ 2. **Data embedded**: Query results are in a `const data = [...]` JavaScript array
546
+ 3. **Data points**: ≤ 500 points total (aggregate or sample if more)
547
+ 4. **Categories**: ≤ 20 categories (use Top N + Other if more)
548
+ 5. **Time granularity**: Appropriate for date range (don't show daily for multi-year)
549
+ 6. **Aggregation**: Never chart raw transactions - always GROUP BY
550
+ 7. **File written**: HTML file saved to /tmp/quarri-chart-*.html
551
+ 8. **Browser opened**: File opened with `open` or `xdg-open`
303
552
 
304
553
  ## Integration
305
554