Deepa Shalini K commited on
Commit
bdae9e2
·
2 Parent(s): c1a412c43728aa

Merge pull request #1 from deepa-shalini-k/custom-dev

Browse files
.gitignore CHANGED
@@ -8,4 +8,13 @@ __pycache__/
8
  # ignore environment variables
9
  .env
10
 
11
- design.html
 
 
 
 
 
 
 
 
 
 
8
  # ignore environment variables
9
  .env
10
 
11
+ # ignore old multi-page files (no longer used)
12
+ pages/
13
+ utils/chartbot_dataset_layout.py
14
+ utils/components.py
15
+
16
+ # ignore design html file
17
+ design.html
18
+
19
+ # ignore temporary files created
20
+ temp*
README.md CHANGED
@@ -1 +1,60 @@
1
- # genai-innovation-challenge-chartbot
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChaRtBot - Data Visualization Assistant
2
+
3
+ A single-page Dash application that helps users visualize their CSV data using natural language prompts powered by AI.
4
+
5
+ ## Features
6
+
7
+ - **Simple, Clean UI**: Modern design with gradient backgrounds and smooth animations
8
+ - **CSV File Upload**: Easy drag-and-drop or click-to-upload functionality (CSV files only)
9
+ - **Natural Language Prompts**: Describe the visualization you want in plain English
10
+ - **AI-Powered Visualizations**: Automatically generates Plotly charts based on your data and prompts
11
+ - **Download Charts**: Export generated visualizations as standalone HTML files
12
+ - **Error Handling**: Comprehensive error handling for file uploads, API calls, and data processing
13
+
14
+ ## How to Use
15
+
16
+ 1. **Start the App**:
17
+ ```bash
18
+ source .venv/bin/activate
19
+ python app.py
20
+ ```
21
+
22
+ 2. **Upload Your Data**: Click "Choose file" and select a CSV file
23
+
24
+ 3. **Enter Your Prompt**: Describe the visualization you want, e.g.:
25
+ - "Create a bar chart showing the top 10 values"
26
+ - "Make a scatter plot with X vs Y"
27
+ - "Show me a pie chart of category distribution"
28
+
29
+ 4. **Submit**: Click the Submit button to generate your visualization
30
+
31
+ 5. **Download** (Optional): Download the generated chart as an HTML file
32
+
33
+ ## Technical Details
34
+
35
+ ### Structure
36
+ - **app.py**: Main application file with all callbacks and layout
37
+ - **utils/prompt.py**: LLM integration for generating visualization code
38
+ - **utils/helpers.py**: Helper functions for processing data and displaying results
39
+ - **assets/custom.css**: Custom styling matching the design specifications
40
+
41
+ ### Callbacks
42
+ 1. **File Upload**: Validates CSV files, stores data in memory
43
+ 2. **Submit**: Validates prompt and data, calls LLM, generates visualizations
44
+ 3. **Download**: Provides HTML export functionality
45
+
46
+ ### Error Handling
47
+ - CSV file validation
48
+ - Missing prompt validation
49
+ - Missing file validation
50
+ - API error handling
51
+ - Code execution error handling
52
+
53
+ ## Requirements
54
+
55
+ See requirements in the Python environment. Main dependencies:
56
+ - Dash
57
+ - Plotly
58
+ - Pandas
59
+ - LangChain (for LLM integration)
60
+ - Dash Mantine Components
app.py CHANGED
@@ -1,134 +1,750 @@
1
  import dash
2
- from dash import html, _dash_renderer
3
  import dash_mantine_components as dmc
4
- import dash_bootstrap_components as dbc
5
- from dash import dcc, callback, Output, Input
6
-
7
- _dash_renderer._set_react_version("18.2.0")
8
 
9
- from utils import components
 
10
 
11
- app = dash.Dash(__name__, use_pages=True, external_stylesheets=[dbc.themes.BOOTSTRAP])
12
-
13
- header = dmc.Flex(
14
  [
15
- dmc.Image(
16
- src="http://geneaipilot.thermofisher.com/assets/tfs.png",
17
- #src=helpers.get_app_file_path("assets", "tfs.png"),
18
- h=35, w=185, className="ps-3 position-absolute top-50 start-0 translate-middle-y"
19
- ),
20
- dmc.Anchor("EmPower.AI", size="xl", fw=530,
21
- href="/", underline="never", c="black",
22
- style={"margin-left": 210},
23
- className="position-absolute top-50 start-0 translate-middle-y"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  ],
25
- direction={"base": "column", "sm": "row"},
26
- gap={"base": "sm", "sm": "lg"},
27
- justify={"sm": "center"},
28
  )
29
 
30
- left_nav_bar = html.Div(
31
- [
32
- dcc.Location(id='url', refresh=True),
33
- dmc.NavLink(
34
- label="Overview",
35
- href="/",
36
- id="nav-overview"
37
- ),
38
- dmc.NavLink(
39
- label="ChaRtBot",
40
- childrenOffset=28,
41
- opened=True,
42
- children=[
43
- dmc.NavLink(label="USA Presidential Elections (1976-2020)", description="Sample dataset", href="/chartbot/us-elections", id="nav-us-elections"),
44
- dmc.NavLink(label="English Women's Football Matches (2011-2024)", description="Sample dataset", href="/chartbot/football", id="nav-football"),
45
- dmc.NavLink(label="Amazon Purchases 2021 (Sample)", description="Sample dataset", href="/chartbot/amazon-purchases", id="nav-amazon-purchases"),
46
- #dmc.NavLink(label="Space Missions (2000-2022)", description="Sample dataset", href="/chartbot/space-missions", id="nav-space-missions"),
47
- dmc.NavLink(label="Upload your data", href="/chartbot", id="nav-upload"),
48
- ],
49
- ),
50
- dmc.NavLink(
51
- label="Product roadmap",
52
- href="/product-roadmap",
53
- id="nav-roadmap",
54
- ),
55
- dmc.NavLink(
56
- label="FAQs",
57
- href="/faqs",
58
- id="nav-faqs",
59
- )
60
- ],
61
- id="navbar",
62
- style={"width": 240, "margin-top": 10, "margin-left": 10, "font-weight": "bold"}
63
  )
 
 
 
64
 
65
- aside = html.Div(
66
- [
67
- dmc.Title("On this page", order=4, style={'padding-bottom': 10, "fontFamily": "Helvetica"}),
68
- components.aside_link("Dataset", "#dataset", "bx:data"),
69
- components.aside_link("Chart", "#chart", "healthicons:chart-line"),
70
- components.aside_link("Python code", "#python-code", "ph:code")
71
- ], style={"padding-top": 100, "margin-left": 50}
 
72
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- app_shell = dmc.AppShell(
75
- [
76
- dcc.Location(id='current-page'),
77
- dmc.AppShellHeader(header),
78
- dmc.AppShellNavbar(left_nav_bar),
79
- dmc.AppShellAside(aside, id="shell-aside", withBorder=False),
80
- dmc.AppShellMain([dash.page_container]),
81
- ],
82
- header={"height": 70},
83
- padding="xl",
84
- navbar={
85
- "width": 260,
86
- "breakpoint": "sm",
87
- "collapsed": {"mobile": True},
88
- },
89
- aside={
90
- "width": 300,
91
- "breakpoint": "xl",
92
- },
93
- id="app-shell"
 
 
 
 
 
 
 
94
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
- app.layout = dmc.MantineProvider([app_shell])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
 
98
  @callback(
99
- Output("app-shell", "aside"),
100
- Input("current-page", "pathname")
101
  )
102
- def toggle_aside(pathname):
103
- if pathname == "/":
104
- return {"width": 0}
105
- return {"width": 300}
 
106
 
 
107
  @callback(
108
- Output("nav-overview", "style"),
109
- Output("nav-us-elections", "style"),
110
- Output("nav-football", "style"),
111
- Output("nav-amazon-purchases", "style"),
112
- #Output("nav-space-missions", "style"),
113
- Output("nav-upload", "style"),
114
- Output("nav-roadmap", "style"),
115
- Output("nav-faqs", "style"),
116
- Input("url", "pathname")
 
 
 
 
117
  )
118
- def update_navlink_styles(pathname):
119
- style = {"backgroundColor": "#FFFFFF"}
120
- active_style = {"backgroundColor": "#EEEEEE"}
 
 
121
 
122
- return [
123
- active_style if pathname == "/" else style,
124
- active_style if pathname == "/chartbot/us-elections" else style,
125
- active_style if pathname == "/chartbot/football" else style,
126
- active_style if pathname == "/chartbot/amazon-purchases" else style,
127
- #active_style if pathname == "/chartbot/space-missions" else style,
128
- active_style if pathname == "/chartbot" else style,
129
- active_style if pathname == "/product-roadmap" else style,
130
- active_style if pathname == "/faqs" else style
131
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  if __name__ == "__main__":
134
  app.run(debug=False)
 
1
  import dash
2
+ from dash import html, dcc, callback, Output, Input, State
3
  import dash_mantine_components as dmc
4
+ import pandas as pd
5
+ from utils import prompt, helpers
6
+ from gallery_data import GALLERY_DATA
 
7
 
8
+ # Initialize the Dash app
9
+ app = dash.Dash(__name__, suppress_callback_exceptions=True)
10
 
11
+ # Define the layout matching design.html
12
+ app.layout = dmc.MantineProvider(
13
+ html.Div(
14
  [
15
+ html.Div(
16
+ [
17
+ html.Div(className="ribbon a", **{"aria-hidden": "true"}),
18
+ html.Div(className="ribbon b", **{"aria-hidden": "true"}),
19
+
20
+ html.Main(
21
+ [
22
+ # Header section
23
+ html.Header(
24
+ [
25
+ html.Div(
26
+ [
27
+ html.Div(className="mark", **{"aria-hidden": "true"}),
28
+ html.Div(
29
+ [
30
+ html.H1("ChaRtBot"),
31
+ html.Div(
32
+ "AI-assisted data visualization through natural language prompts",
33
+ className="sub"
34
+ )
35
+ ]
36
+ )
37
+ ],
38
+ className="brand"
39
+ ),
40
+ # Container for tabs and button
41
+ html.Div(
42
+ [
43
+ html.Button(
44
+ "New Chart",
45
+ id="new-chart-button",
46
+ className="pill",
47
+ n_clicks=0,
48
+ style={
49
+ "cursor": "pointer",
50
+ "border": "none",
51
+ "fontFamily": "inherit"
52
+ }
53
+ ),
54
+ # Tab switcher
55
+ html.Div(
56
+ [
57
+ html.Button(
58
+ "CREATE",
59
+ id="tab-create",
60
+ className="tab active",
61
+ type="button",
62
+ style={
63
+ "height": "30px",
64
+ "padding": "0 16px",
65
+ },
66
+ **{
67
+ "role": "tab",
68
+ "aria-selected": "true"
69
+ }
70
+ ),
71
+ html.Button(
72
+ "GALLERY",
73
+ id="tab-gallery",
74
+ className="tab",
75
+ type="button",
76
+ style={
77
+ "height": "30px",
78
+ "padding": "0 16px",
79
+ },
80
+ **{
81
+ "role": "tab",
82
+ "aria-selected": "false"
83
+ }
84
+ )
85
+ ],
86
+ className="tabs",
87
+ role="tablist",
88
+ **{"aria-label": "ChaRtBot pages"}
89
+ )
90
+ ],
91
+ style={
92
+ "display": "flex",
93
+ "alignItems": "center",
94
+ "gap": "12px"
95
+ }
96
+ )
97
+ ]
98
+ ),
99
+
100
+ # Visualizer Page (Form section)
101
+ html.Div(
102
+ [
103
+ # Prompt textarea
104
+ html.Div(
105
+ [
106
+ dcc.Textarea(
107
+ id="prompt-textarea",
108
+ placeholder='Example: "Create a heatmap of weekly sales by month, highlighting high-volume days"',
109
+ style={
110
+ "width": "100%",
111
+ "height": "120px",
112
+ "resize": "none",
113
+ "border": "none",
114
+ "outline": "none",
115
+ "background": "transparent",
116
+ "color": "var(--ink)",
117
+ "fontSize": "15px",
118
+ "lineHeight": "1.55"
119
+ }
120
+ )
121
+ ],
122
+ className="prompt"
123
+ ),
124
+
125
+ # How it works section
126
+ html.Div(
127
+ [
128
+ html.Span("How it works ", style={
129
+ "fontSize": "13px",
130
+ "color": "#667085",
131
+ "fontWeight": "500"
132
+ }),
133
+ html.Span("ⓘ", id="info-icon", style={
134
+ "cursor": "pointer",
135
+ "fontSize": "14px",
136
+ "color": "#6941C6",
137
+ "marginLeft": "2px"
138
+ })
139
+ ],
140
+ style={
141
+ "marginTop": "8px",
142
+ "marginBottom": "16px"
143
+ }
144
+ ),
145
+
146
+ # Drawer for How it works
147
+ dmc.Drawer(
148
+ title="How it works",
149
+ id="info-drawer",
150
+ padding="md",
151
+ size="400px",
152
+ position="right",
153
+ children=[
154
+ html.Div([
155
+ html.Ol([
156
+ html.Li([
157
+ html.Strong("Upload and review your data:"),
158
+ " Choose a CSV file containing your dataset and take a moment to explore it—understand the columns, data types, and any patterns, filters, or transformations relevant to your analysis."
159
+ ], style={"marginBottom": "16px"}),
160
+ html.Li([
161
+ html.Strong("Describe your analytical goal:"),
162
+ " Write a natural language prompt that clearly states what you want to analyze and visualize. Reference specific column names and describe how they should be used (e.g., grouping, aggregation, filtering)."
163
+ ], style={"marginBottom": "16px"}),
164
+ html.Li([
165
+ html.Strong("Refine your prompt if needed:"),
166
+ " If the output isn’t what you expected or an error occurs, adjust your prompt to be more specific or clarify assumptions. Small changes often lead to better results."
167
+ ], style={"marginBottom": "16px"}),
168
+ html.Li([
169
+ html.Strong("Review the generated code and chart:"),
170
+ " Once the visualization is generated, review the underlying code to ensure it accurately reflects your analytical intent before using or sharing the results."
171
+ ], style={"marginBottom": "16px"}),
172
+ html.Li([
173
+ html.Strong("Share or export your results:"),
174
+ " Download the visualization as an interactive HTML file to preserve interactivity, or export it as a static image for reports, presentations, or documentation."
175
+ ], style={"marginBottom": "0px"}),
176
+ ], style={
177
+ "fontSize": "13px",
178
+ "lineHeight": "1.6",
179
+ "color": "#344054",
180
+ "paddingLeft": "20px"
181
+ }),
182
+ html.Div([
183
+ html.H4("Tips for better results:", style={
184
+ "fontSize": "13px",
185
+ "fontWeight": "600",
186
+ "color": "#344054",
187
+ "marginTop": "24px",
188
+ "marginBottom": "12px"
189
+ }),
190
+ html.Ul([
191
+ html.Li("Mention specific column names from your dataset"),
192
+ html.Li("Specify colors, themes, or styling preferences"),
193
+ html.Li("Include aggregations or transformations you need"),
194
+ html.Li("Be clear about labels, titles, and legends")
195
+ ], style={
196
+ "fontSize": "12px",
197
+ "lineHeight": "1.6",
198
+ "color": "#475467",
199
+ "paddingLeft": "20px"
200
+ })
201
+ ])
202
+ ])
203
+ ]
204
+ ),
205
+
206
+ # File picker and submit button row
207
+ html.Div(
208
+ [
209
+ html.Div(
210
+ [
211
+ dmc.Tooltip(
212
+ label="Only CSV files are supported",
213
+ position="right",
214
+ withArrow=True,
215
+ children=[
216
+ dcc.Upload(
217
+ id="upload-data",
218
+ children=html.Button(
219
+ "Choose file",
220
+ className="pickBtn",
221
+ type="button",
222
+ style={
223
+ "height": "40px",
224
+ "padding": "0 20px",
225
+ "minWidth": "120px",
226
+ "fontFamily": "inherit"
227
+ }
228
+ ),
229
+ accept=".csv",
230
+ multiple=False
231
+ )
232
+ ]
233
+ ),
234
+ html.Div(id="file-name-display", style={"fontSize": "12px", "color": "#475467", "marginTop": "8px"})
235
+ ],
236
+ style={"display": "flex", "alignItems": "center", "gap": "12px"}
237
+ ),
238
+ dcc.Loading(
239
+ id="loading",
240
+ type="default",
241
+ children=html.Button(
242
+ "Visualize",
243
+ id="submit-button",
244
+ className="submitBtn",
245
+ type="button",
246
+ n_clicks=0,
247
+ disabled=True,
248
+ style={
249
+ "height": "40px",
250
+ "padding": "0 20px",
251
+ "minWidth": "120px",
252
+ "fontFamily": "inherit"
253
+ }
254
+ )
255
+ )
256
+ ],
257
+ className="row"
258
+ ),
259
+
260
+ # Output sections
261
+ html.A(
262
+ "Download Chart as HTML",
263
+ id="download-html",
264
+ download="chart.html",
265
+ href="",
266
+ target="_blank",
267
+ style={"display": "none", "marginTop": "20px", "textAlign": "right"}
268
+ ),
269
+ html.Div(id="dataset-explorer", style={"marginTop": "10px"}),
270
+ html.Div(id="chartbot-output", style={"marginTop": "10px"}),
271
+ html.Div(id="python-content-output", style={"marginTop": "10px"}),
272
+
273
+ # Footer section with notes and disclaimers
274
+ html.Div(
275
+ [
276
+ html.Div(
277
+ [
278
+ html.H3("Notes and Disclaimers", style={
279
+ "fontSize": "12px",
280
+ "fontWeight": "600",
281
+ "color": "#667085",
282
+ "marginBottom": "8px",
283
+ "letterSpacing": "0.02em"
284
+ }),
285
+ html.Ul([
286
+ html.Li("AI-generated outputs may contain errors. Users should review their data, prompts, and generated code to verify that visualizations accurately reflect their intended analysis. Human oversight is required before use in decision-making."),
287
+ html.Li("ChaRtBot does not store, log, or retain user-provided datasets, prompts, generated code, or visualization outputs. All processing is performed transiently during the active session.")
288
+ ], style={
289
+ "fontSize": "11px",
290
+ "color": "#69707D",
291
+ "lineHeight": "1.6",
292
+ "margin": "0",
293
+ "paddingLeft": "16px"
294
+ })
295
+ ],
296
+ style={"marginBottom": "20px"}
297
+ ),
298
+ html.Div(
299
+ [
300
+ html.H3("About this project", style={
301
+ "fontSize": "12px",
302
+ "fontWeight": "600",
303
+ "color": "#667085",
304
+ "marginBottom": "8px",
305
+ "letterSpacing": "0.02em"
306
+ }),
307
+ html.P([
308
+ "ChaRtBot is a personal project created by Deepa Shalini K to explore AI-assisted data visualization and user-centered analytical workflows."
309
+ ], style={
310
+ "fontSize": "11px",
311
+ "color": "#69707D",
312
+ "lineHeight": "1.6",
313
+ "margin": "0 0 8px 0"
314
+ }),
315
+ html.P([
316
+ html.Strong("Contact: ", style={"fontWeight": "600"}),
317
+ html.A("Email", href="mailto:shalini.jul97@gmail.com", target="_blank", style={
318
+ "color": "#6941C6",
319
+ "textDecoration": "none"
320
+ }),
321
+ " · ",
322
+ html.A("LinkedIn", href="https://www.linkedin.com/in/deepa-shalini-273385193/", target="_blank", style={
323
+ "color": "#6941C6",
324
+ "textDecoration": "none"
325
+ })
326
+ ], style={
327
+ "fontSize": "11px",
328
+ "color": "#98A2B3",
329
+ "lineHeight": "1.6",
330
+ "margin": "0"
331
+ })
332
+ ]
333
+ ),
334
+ html.P("© 2026 Deepa Shalini K. All rights reserved.", style={
335
+ "fontSize": "10px",
336
+ "color": "#434447",
337
+ "textAlign": "center",
338
+ "marginTop": "24px",
339
+ "marginBottom": "0",
340
+ "paddingTop": "20px",
341
+ "borderTop": "1px solid #F2F4F7"
342
+ })
343
+ ],
344
+ style={
345
+ "marginTop": "5px",
346
+ "padding": "10px 0 16px 0"
347
+ }
348
+ ),
349
+
350
+ # Hidden stores for data
351
+ dcc.Store(id="stored-data"),
352
+ dcc.Store(id="stored-file-name"),
353
+ dcc.Store(id="html-buffer")
354
+ ],
355
+ id="visualizer-page",
356
+ style={"marginTop": "10px"}
357
+ ),
358
+
359
+ # Gallery Page
360
+ html.Div(
361
+ [
362
+ # Gallery page header
363
+ html.Section(
364
+ html.Div(
365
+ [
366
+ html.H2("Gallery", style={"margin": "0", "fontSize": "18px", "letterSpacing": "-0.02em"}),
367
+ html.P(
368
+ "Browse charts generated by ChaRtBot — each card includes the prompt and the original dataset.",
369
+ style={"margin": "6px 0 0", "fontSize": "13px", "color": "var(--muted)"}
370
+ )
371
+ ],
372
+ className="page-title"
373
+ ),
374
+ className="page-head"
375
+ ),
376
+
377
+ # Gallery grid
378
+ html.Section(
379
+ [
380
+ # Generate cards from GALLERY_DATA
381
+ *[
382
+ html.Article(
383
+ [
384
+ html.Div(
385
+ [
386
+ html.Img(
387
+ src=item["image"],
388
+ alt=f"Chart thumbnail {i+1}"
389
+ )
390
+ ],
391
+ className="thumb"
392
+ ),
393
+ html.Div(
394
+ [
395
+ html.P(
396
+ item["prompt"],
397
+ className="gallery-prompt"
398
+ ),
399
+ html.Div(
400
+ [
401
+ html.Div(
402
+ [
403
+ html.A(
404
+ "CSV",
405
+ className="link",
406
+ href=item["csv_link"],
407
+ target="_blank" if item["csv_link"] != "#" else ""
408
+ ),
409
+ html.Span(
410
+ item["badge"],
411
+ className="badge"
412
+ ) if item.get("badge") else None
413
+ ],
414
+ className="links"
415
+ )
416
+ ],
417
+ className="meta"
418
+ )
419
+ ],
420
+ className="content"
421
+ )
422
+ ],
423
+ className="gallery-card"
424
+ )
425
+ for i, item in enumerate(GALLERY_DATA)
426
+ ]
427
+ ],
428
+ className="gallery-grid",
429
+ **{"aria-label": "Gallery grid"}
430
+ ),
431
+
432
+ # Copyright footer for gallery page
433
+ html.Div(
434
+ html.P("© 2026 Deepa Shalini K. All rights reserved.", style={
435
+ "fontSize": "10px",
436
+ "color": "#434447",
437
+ "textAlign": "center",
438
+ "marginTop": "20px",
439
+ "marginBottom": "0",
440
+ "paddingTop": "10px",
441
+ "borderTop": "1px solid #F2F4F7"
442
+ }),
443
+ style={
444
+ "marginTop": "10px",
445
+ "padding": "0 0 16px 0"
446
+ }
447
+ )
448
+ ],
449
+ id="gallery-page",
450
+ style={"display": "none"}
451
+ )
452
+ ],
453
+ className="card",
454
+ role="main"
455
+ )
456
+ ],
457
+ className="shell"
458
+ )
459
  ],
460
+ className="viewport"
461
+ )
 
462
  )
463
 
464
+ # Add callback for drawer
465
+ @callback(
466
+ Output("info-drawer", "opened"),
467
+ Input("info-icon", "n_clicks"),
468
+ State("info-drawer", "opened"),
469
+ prevent_initial_call=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
470
  )
471
+ def toggle_drawer(n_clicks, opened):
472
+ """Toggle the info drawer when the info icon is clicked."""
473
+ return not opened
474
 
475
+ # Callback for file upload
476
+ @callback(
477
+ Output("stored-data", "data"),
478
+ Output("stored-file-name", "data"),
479
+ Output("file-name-display", "children"),
480
+ Output("dataset-explorer", "children"), # Add this output
481
+ Input("upload-data", "contents"),
482
+ State("upload-data", "filename")
483
  )
484
+ def upload_file(contents, filename):
485
+ """Handle CSV file upload and store the data."""
486
+ if contents is None:
487
+ return None, None, None, None # Add None for dataset-explorer
488
+
489
+ try:
490
+ # Parse the uploaded file
491
+ content_type, content_string = contents.split(",")
492
+ import base64
493
+ import io
494
+ decoded = base64.b64decode(content_string)
495
+
496
+ # Only accept CSV files
497
+ if not filename.endswith('.csv'):
498
+ return None, None, html.Div("Only CSV files are allowed", style={"color": "red"}), None
499
+
500
+ # Read CSV file
501
+ df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
502
+
503
+ # Create AG Grid accordion
504
+ grid_accordion = html.Div([
505
+ dmc.Accordion(
506
+ children=[
507
+ dmc.AccordionItem(
508
+ [
509
+ dmc.AccordionControl(
510
+ html.Div([
511
+ html.Span("Explore Dataset", style={"fontWeight": "600", "fontSize": "15px"}),
512
+ html.Span(f" ({len(df)} rows, {len(df.columns)} columns)",
513
+ style={"fontSize": "13px", "color": "#667085", "marginLeft": "8px"})
514
+ ])
515
+ ),
516
+ dmc.AccordionPanel(
517
+ helpers.create_ag_grid(df)
518
+ )
519
+ ],
520
+ value="dataset"
521
+ )
522
+ ],
523
+ chevronPosition="right",
524
+ variant="filled",
525
+ style={
526
+ "border": "2px solid #E4E7EC",
527
+ "borderRadius": "8px",
528
+ "boxShadow": "0 1px 3px rgba(0, 0, 0, 0.1)"
529
+ }
530
+ )
531
+ ], style={"marginTop": "20px"})
532
+
533
+ return df.to_dict("records"), filename, html.Div(f"✓ {filename}", style={"color": "#067A55", "fontWeight": "600"}), grid_accordion
534
+
535
+ except Exception as e:
536
+ return None, None, html.Div(f"Error: {str(e)}", style={"color": "red"}), None
537
 
538
+ # Callback to enable/disable submit button based on inputs
539
+ @callback(
540
+ Output("submit-button", "disabled", allow_duplicate=True),
541
+ Input("prompt-textarea", "value"),
542
+ Input("stored-data", "data"),
543
+ Input("stored-file-name", "data"),
544
+ prevent_initial_call=True
545
+ )
546
+ def toggle_submit_button(user_prompt, stored_data, filename):
547
+ """Enable submit button only when both prompt and file are provided."""
548
+ # Disable button if prompt is empty or file is not uploaded
549
+ if not user_prompt or not user_prompt.strip() or not stored_data or not filename:
550
+ return True
551
+ return False
552
+
553
+ # Callback for submit button with validation
554
+ @callback(
555
+ Output("chartbot-output", "children"),
556
+ Output("python-content-output", "children"),
557
+ Output("download-html", "style"),
558
+ Output("html-buffer", "data"),
559
+ Output("submit-button", "disabled"),
560
+ Input("submit-button", "n_clicks"),
561
+ State("prompt-textarea", "value"),
562
+ State("stored-data", "data"),
563
+ State("stored-file-name", "data"),
564
+ prevent_initial_call=True
565
  )
566
+ def create_graph(n_clicks, user_prompt, stored_data, filename):
567
+ """Create visualization based on user prompt and uploaded CSV data."""
568
+ if n_clicks == 0:
569
+ return None, None, {"display": "none"}, None, False
570
+
571
+ try:
572
+ # Validate inputs
573
+ if not user_prompt or not user_prompt.strip():
574
+ return html.Div([
575
+ html.Br(),
576
+ dmc.Alert("Please enter a prompt for visualization.", title="Missing Prompt", color="red")
577
+ ]), None, {"display": "none"}, None, False
578
+
579
+ if not stored_data or not filename:
580
+ return html.Div([
581
+ html.Br(),
582
+ dmc.Alert("Please upload a CSV file before submitting.", title="Missing File", color="red")
583
+ ]), None, {"display": "none"}, None, False
584
+
585
+ # Convert stored data back to DataFrame
586
+ df = pd.DataFrame(stored_data)
587
+
588
+ # Save the dataframe temporarily for processing
589
+ import os
590
+ temp_file_path = os.path.join(os.getcwd(), "temp_uploaded_data.csv")
591
+ df.to_csv(temp_file_path, index=False)
592
+
593
+ # Get first 5 rows as CSV string
594
+ df_5_rows = df.head(5)
595
+ data_top5_csv_string = df_5_rows.to_csv(index=False)
596
+
597
+ # Get response from LLM
598
+ result_output = prompt.get_response(user_prompt, data_top5_csv_string, temp_file_path)
599
+
600
+ # Display the response - returns 5 values
601
+ graph, code, download_style, html_buffer, _ = helpers.display_response(result_output, temp_file_path)
602
+
603
+ # Extract the Python code from result_output for the accordion
604
+ import re
605
+ code_block_match = re.search(r"```(?:[Pp]ython)?(.*?)```", result_output, re.DOTALL)
606
+ python_code = code_block_match.group(1).strip() if code_block_match else "No code found"
607
+
608
+ # Create accordion with the generated code
609
+ code_accordion = html.Div([
610
+ dmc.Accordion(
611
+ children=[
612
+ dmc.AccordionItem(
613
+ [
614
+ dmc.AccordionControl(
615
+ html.Div([
616
+ html.Span("Code Generated", style={"fontWeight": "600", "fontSize": "15px"})
617
+ ])
618
+ ),
619
+ dmc.AccordionPanel(
620
+ html.Div([
621
+ html.Div([
622
+ dcc.Clipboard(
623
+ target_id="code-display",
624
+ title="Copy code",
625
+ style={
626
+ "position": "absolute",
627
+ "top": "12px",
628
+ "right": "12px",
629
+ "fontSize": "18px",
630
+ "cursor": "pointer",
631
+ "padding": "8px",
632
+ "border": "1px solid #d0d5dd",
633
+ "borderRadius": "6px",
634
+ "display": "inline-flex",
635
+ "alignItems": "center",
636
+ "justifyContent": "center",
637
+ "color": "#475467",
638
+ "transition": "all 0.2s",
639
+ "zIndex": "10",
640
+ "width": "32px",
641
+ "height": "32px"
642
+ }
643
+ )
644
+ ], style={"position": "relative"}),
645
+ html.Pre(
646
+ html.Code(
647
+ python_code,
648
+ id="code-display",
649
+ style={
650
+ "fontSize": "13px",
651
+ "lineHeight": "1.6",
652
+ "fontFamily": "'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace",
653
+ "backgroundColor": "#f6f8fa",
654
+ "padding": "16px",
655
+ "paddingTop": "48px",
656
+ "borderRadius": "6px",
657
+ "display": "block",
658
+ "overflowX": "auto",
659
+ "color": "#24292f"
660
+ }
661
+ ),
662
+ style={"margin": "0", "position": "relative"}
663
+ )
664
+ ], style={"position": "relative"})
665
+ )
666
 
667
+ ],
668
+ value="code"
669
+ )
670
+ ],
671
+ chevronPosition="right",
672
+ variant="filled",
673
+ style={
674
+ "border": "2px solid #E4E7EC",
675
+ "borderRadius": "8px",
676
+ "boxShadow": "0 1px 3px rgba(0, 0, 0, 0.1)"
677
+ }
678
+ )
679
+ ])
680
+
681
+ return graph, code_accordion, {"display": "block", "textAlign": "right", "marginTop": "20px"}, html_buffer, True
682
+
683
+ except Exception as e:
684
+ error_message = str(e)
685
+ return html.Div([
686
+ html.Br(),
687
+ dmc.Alert(error_message, title="Error", color="red")
688
+ ]), None, {"display": "none"}, None, False
689
 
690
+ # Callback for download HTML
691
  @callback(
692
+ Output("download-html", "href"),
693
+ Input("html-buffer", "data")
694
  )
695
+ def download_html(encoded):
696
+ """Generate download link for the chart as HTML."""
697
+ if encoded:
698
+ return f"data:text/html;base64,{encoded}"
699
+ return ""
700
 
701
+ # Callback for New Chat button to reset everything
702
  @callback(
703
+ Output("prompt-textarea", "value"),
704
+ Output("stored-data", "data", allow_duplicate=True),
705
+ Output("stored-file-name", "data", allow_duplicate=True),
706
+ Output("file-name-display", "children", allow_duplicate=True),
707
+ Output("dataset-explorer", "children", allow_duplicate=True), # Add this output
708
+ Output("chartbot-output", "children", allow_duplicate=True),
709
+ Output("python-content-output", "children", allow_duplicate=True),
710
+ Output("download-html", "style", allow_duplicate=True),
711
+ Output("html-buffer", "data", allow_duplicate=True),
712
+ Output("submit-button", "disabled", allow_duplicate=True),
713
+ Output("upload-data", "contents"),
714
+ Input("new-chart-button", "n_clicks"),
715
+ prevent_initial_call=True
716
  )
717
+ def reset_chat(n_clicks):
718
+ """Reset all inputs and outputs to start a new chat."""
719
+ if n_clicks > 0:
720
+ return "", None, None, None, None, None, None, {"display": "none"}, None, True, None # Added None for dataset-explorer
721
+ return dash.no_update
722
 
723
+ # Callback for tab switching
724
+ @callback(
725
+ Output("tab-create", "className"),
726
+ Output("tab-gallery", "className"),
727
+ Output("visualizer-page", "style"),
728
+ Output("gallery-page", "style"),
729
+ Output("new-chart-button", "style"),
730
+ Input("tab-create", "n_clicks"),
731
+ Input("tab-gallery", "n_clicks"),
732
+ prevent_initial_call=True
733
+ )
734
+ def switch_tabs(visualizer_clicks, gallery_clicks):
735
+ """Handle tab switching between Visualizer and Gallery."""
736
+ ctx = dash.callback_context
737
+ if not ctx.triggered:
738
+ return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"}
739
+
740
+ button_id = ctx.triggered[0]["prop_id"].split(".")[0]
741
+
742
+ if button_id == "tab-create":
743
+ return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"}
744
+ elif button_id == "tab-gallery":
745
+ return "tab", "tab active", {"display": "none"}, {"marginTop": "10px"}, {"display": "none"}
746
+
747
+ return "tab active", "tab", {"marginTop": "10px"}, {"display": "none"}, {"cursor": "pointer", "border": "none", "fontFamily": "inherit"}
748
 
749
  if __name__ == "__main__":
750
  app.run(debug=False)
assets/custom.css CHANGED
@@ -1,8 +1,464 @@
1
- .app-title {
2
- font-size: 3em;
3
- font-family: Helvetica;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- body, a, h1, h2, h3, h4, h5, h6 {
7
- font-family: Helvetica;
 
 
8
  }
 
1
+ /* Import Manrope font */
2
+ @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap');
3
+
4
+ :root {
5
+ --bg: #F7F8FB;
6
+ --card: #FFFFFF;
7
+ --ink: #101828;
8
+ --muted: #475467;
9
+ --border: #E4E7EC;
10
+ --cobalt: #2563EB;
11
+ --mint: #10B981;
12
+ --wash-blue: #EEF4FF;
13
+ --wash-green: #ECFDF3;
14
+ --shadow: 0 18px 60px rgba(16, 24, 40, .10);
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+
21
+ body {
22
+ margin: 0;
23
+ min-height: 100vh;
24
+ font-family: "Manrope", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
25
+ color: var(--ink);
26
+ background:
27
+ radial-gradient(900px 600px at 18% 18%, rgba(37, 99, 235, .10), transparent 60%),
28
+ radial-gradient(900px 600px at 82% 22%, rgba(16, 185, 129, .10), transparent 62%),
29
+ linear-gradient(180deg, var(--bg), #FFFFFF);
30
+ }
31
+
32
+ .viewport {
33
+ min-height: 100vh;
34
+ display: grid;
35
+ place-items: center;
36
+ padding: 28px 16px;
37
+ }
38
+
39
+ .shell {
40
+ width: min(1400px, 85vw);
41
+ position: relative;
42
+ }
43
+
44
+ .ribbon {
45
+ position: absolute;
46
+ width: 320px;
47
+ height: 140px;
48
+ border-radius: 999px;
49
+ filter: blur(28px);
50
+ opacity: .55;
51
+ pointer-events: none;
52
+ }
53
+
54
+ .ribbon.a {
55
+ top: -40px;
56
+ left: -40px;
57
+ background: rgba(37, 99, 235, .22);
58
+ }
59
+
60
+ .ribbon.b {
61
+ bottom: -50px;
62
+ right: -30px;
63
+ background: rgba(16, 185, 129, .22);
64
+ }
65
+
66
+ .card {
67
+ background: var(--card);
68
+ border: 1px solid var(--border);
69
+ border-radius: 26px;
70
+ box-shadow: var(--shadow);
71
+ padding: 26px;
72
+ }
73
+
74
+ header {
75
+ display: flex;
76
+ gap: 14px;
77
+ align-items: flex-start;
78
+ justify-content: space-between;
79
+ flex-wrap: wrap;
80
+ margin-bottom: 14px;
81
+ }
82
+
83
+ /* Tab switcher styles */
84
+ .tabs {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 10px;
88
+ padding: 6px;
89
+ background: rgba(255, 255, 255, 0.70);
90
+ border: 1px solid var(--border);
91
+ border-radius: 999px;
92
+ box-shadow: 0 10px 20px rgba(16, 24, 40, 0.05);
93
+ }
94
+
95
+ .tab {
96
+ border: 0;
97
+ background: transparent;
98
+ padding: 10px 14px;
99
+ border-radius: 999px;
100
+ font-weight: 600;
101
+ font-size: 12px;
102
+ letter-spacing: 0.08em;
103
+ text-transform: uppercase;
104
+ color: rgba(15, 23, 42, 0.70);
105
+ cursor: pointer;
106
+ font-family: inherit;
107
+ transition: all 0.2s ease;
108
+ }
109
+
110
+ .tab:focus {
111
+ outline: none;
112
+ box-shadow: 0 0 0 4px rgba(47, 107, 255, 0.18);
113
+ }
114
+
115
+ .tab.active {
116
+ color: #0b2a7a;
117
+ background: linear-gradient(180deg, rgba(47, 107, 255, 0.18), rgba(47, 107, 255, 0.08));
118
+ border: 1px solid rgba(47, 107, 255, 0.22);
119
+ }
120
+
121
+ .tab:hover:not(.active) {
122
+ background: rgba(47, 107, 255, 0.05);
123
+ }
124
+
125
+ .brand {
126
+ display: flex;
127
+ gap: 12px;
128
+ align-items: flex-start;
129
+ min-width: 260px;
130
+ }
131
+
132
+ .mark {
133
+ width: 44px;
134
+ height: 44px;
135
+ border-radius: 16px;
136
+ background:
137
+ radial-gradient(14px 14px at 30% 30%, rgba(255, 255, 255, .9), transparent 60%),
138
+ linear-gradient(135deg, var(--cobalt), var(--mint));
139
+ box-shadow: 0 14px 30px rgba(37, 99, 235, .18);
140
+ flex: 0 0 auto;
141
+ }
142
+
143
+ h1 {
144
+ margin: 0;
145
+ font-size: 22px;
146
+ line-height: 1.2;
147
+ letter-spacing: -0.02em;
148
+ }
149
+
150
+ .sub {
151
+ margin-top: 6px;
152
+ color: var(--muted);
153
+ font-size: 13.5px;
154
+ line-height: 1.5;
155
+ max-width: 62ch;
156
+ }
157
+
158
+ .pill {
159
+ border: 1px solid rgba(37, 99, 235, .22);
160
+ background: var(--wash-blue);
161
+ color: #1D4ED8;
162
+ border-radius: 999px;
163
+ padding: 8px 12px;
164
+ font-size: 12px;
165
+ font-weight: 800;
166
+ letter-spacing: .12em;
167
+ text-transform: uppercase;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ .prompt {
172
+ border: 1px solid var(--border);
173
+ border-radius: 18px;
174
+ background:
175
+ linear-gradient(180deg, var(--wash-blue), transparent 70%),
176
+ #fff;
177
+ padding: 14px;
178
+ }
179
+
180
+ .row {
181
+ display: flex;
182
+ align-items: stretch;
183
+ justify-content: space-between;
184
+ gap: 12px;
185
+ flex-wrap: wrap;
186
+ margin-top: 12px;
187
+ }
188
+
189
+ .pickBtn {
190
+ border: 1px solid rgba(37, 99, 235, .18);
191
+ background: var(--wash-blue);
192
+ color: #1D4ED8;
193
+ border-radius: 14px;
194
+ padding: 10px 12px;
195
+ cursor: pointer;
196
+ font-weight: 800;
197
+ font-size: 12px;
198
+ letter-spacing: .06em;
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 8px;
202
+ transition: transform .12s ease, filter .12s ease;
203
+ user-select: none;
204
+ }
205
+
206
+ .pickBtn:hover {
207
+ filter: brightness(.99);
208
+ }
209
+
210
+ .pickBtn:active {
211
+ transform: translateY(1px);
212
+ }
213
+
214
+ .submitBtn {
215
+ border: none;
216
+ border-radius: 16px;
217
+ padding: 12px 16px;
218
+ cursor: pointer;
219
+ font-weight: 900;
220
+ color: #fff;
221
+ background: linear-gradient(135deg, var(--cobalt), #1D4ED8);
222
+ box-shadow: 0 14px 30px rgba(37, 99, 235, .22);
223
+ transition: transform .12s ease, filter .12s ease;
224
+ flex: 0 0 auto;
225
+ }
226
+
227
+ .submitBtn:hover {
228
+ filter: brightness(.98);
229
+ }
230
+
231
+ .submitBtn:active {
232
+ transform: translateY(1px);
233
+ }
234
+
235
+ .submitBtn:disabled {
236
+ background: linear-gradient(135deg, #94A3B8, #CBD5E1);
237
+ box-shadow: none;
238
+ cursor: not-allowed;
239
+ opacity: 0.6;
240
+ }
241
+
242
+ .submitBtn:disabled:hover {
243
+ filter: none;
244
+ transform: none;
245
+ }
246
+
247
+ .submitBtn:disabled:active {
248
+ transform: none;
249
+ }
250
+
251
+ .icon {
252
+ width: 18px;
253
+ height: 18px;
254
+ display: block;
255
+ }
256
+
257
+ .icon-blue path {
258
+ stroke: #1D4ED8;
259
+ }
260
+
261
+ @media (max-width: 560px) {
262
+ .row {
263
+ align-items: stretch;
264
+ }
265
+ .submitBtn {
266
+ width: 100%;
267
+ }
268
+ }
269
+
270
+ /* Gallery Page Styles */
271
+ .page-head {
272
+ display: flex;
273
+ align-items: flex-end;
274
+ justify-content: space-between;
275
+ gap: 18px;
276
+ padding: 8px 10px 14px;
277
+ }
278
+
279
+ .page-title h2 {
280
+ margin: 0;
281
+ font-size: 18px;
282
+ letter-spacing: -0.02em;
283
+ }
284
+
285
+ .page-title p {
286
+ margin: 6px 0 0;
287
+ font-size: 13px;
288
+ color: var(--muted);
289
+ }
290
+
291
+ /* Gallery grid: 5 rows x 4 columns = 20 items */
292
+ .gallery-grid {
293
+ margin: 8px 6px 0;
294
+ padding: 14px 10px 8px;
295
+ display: grid;
296
+ grid-template-columns: repeat(4, minmax(0, 1fr));
297
+ gap: 14px;
298
+ }
299
+
300
+ /* Gallery Cards */
301
+ .gallery-card {
302
+ background: rgba(255, 255, 255, 0.82);
303
+ border: 1px solid var(--border);
304
+ border-radius: 22px;
305
+ box-shadow: 0 10px 26px rgba(16, 24, 40, 0.08);
306
+ overflow: hidden;
307
+ position: relative;
308
+ transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
309
+ display: flex;
310
+ flex-direction: column;
311
+ min-height: 250px;
312
+ }
313
+
314
+ .gallery-card:hover {
315
+ transform: translateY(-2px);
316
+ border-color: rgba(47, 107, 255, 0.18);
317
+ box-shadow: 0 18px 44px rgba(16, 24, 40, 0.12);
318
+ }
319
+
320
+ .gallery-card:focus-within {
321
+ box-shadow: 0 0 0 4px rgba(31, 209, 192, 0.14), 0 18px 44px rgba(16, 24, 40, 0.12);
322
+ }
323
+
324
+ /* Thumbnail area */
325
+ .thumb {
326
+ aspect-ratio: 1 / 1;
327
+ border-bottom: 1px solid rgba(15, 23, 42, 0.06);
328
+ background:
329
+ radial-gradient(260px 120px at 20% 30%, rgba(79, 172, 254, 0.25), transparent 60%),
330
+ radial-gradient(260px 120px at 80% 40%, rgba(31, 209, 192, 0.18), transparent 62%),
331
+ linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.6));
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ position: relative;
336
+ }
337
+
338
+ .thumb img {
339
+ width: 92%;
340
+ height: 88%;
341
+ object-fit: contain;
342
+ border-radius: 14px;
343
+ border: 1px solid rgba(15, 23, 42, 0.10);
344
+ box-shadow: 0 12px 22px rgba(16, 24, 40, 0.10);
345
+ background: white;
346
+ }
347
+
348
+ /* Badge/Chip styling */
349
+ .badge {
350
+ display: inline-flex;
351
+ align-items: center;
352
+ padding: 4px 10px;
353
+ font-size: 11px;
354
+ font-weight: 600;
355
+ letter-spacing: 0.03em;
356
+ text-transform: uppercase;
357
+ color: #2f6bff;
358
+ background: rgba(47, 107, 255, 0.1);
359
+ border: 1px solid rgba(47, 107, 255, 0.2);
360
+ border-radius: 999px;
361
+ white-space: nowrap;
362
+ }
363
+
364
+ /* Content */
365
+ .content {
366
+ padding: 12px 12px 12px;
367
+ display: flex;
368
+ flex-direction: column;
369
+ gap: 10px;
370
+ flex: 1;
371
+ }
372
+
373
+ .gallery-prompt {
374
+ margin: 0;
375
+ font-size: 12.5px;
376
+ line-height: 1.35;
377
+ color: rgba(15, 23, 42, 0.78);
378
+ display: -webkit-box;
379
+ -webkit-box-orient: vertical;
380
+ overflow: hidden;
381
+ min-height: calc(1.35em * 3);
382
+ }
383
+
384
+ .meta {
385
+ margin-top: auto;
386
+ display: flex;
387
+ align-items: center;
388
+ justify-content: space-between;
389
+ gap: 10px;
390
+ }
391
+
392
+ .links {
393
+ display: flex;
394
+ gap: 8px;
395
+ flex-wrap: wrap;
396
+ }
397
+
398
+ .link {
399
+ display: inline-flex;
400
+ align-items: center;
401
+ gap: 6px;
402
+ padding: 7px 10px;
403
+ border-radius: 999px;
404
+ font-size: 12px;
405
+ font-weight: 700;
406
+ color: rgba(15, 23, 42, 0.74);
407
+ border: 1px solid var(--border);
408
+ background: rgba(255, 255, 255, 0.75);
409
+ text-decoration: none;
410
+ transition: all 0.15s ease;
411
+ }
412
+
413
+ .link:hover {
414
+ border-color: rgba(47, 107, 255, 0.22);
415
+ color: rgba(11, 42, 122, 0.95);
416
+ }
417
+
418
+ .link:focus {
419
+ outline: none;
420
+ box-shadow: 0 0 0 4px rgba(47, 107, 255, 0.18);
421
+ }
422
+
423
+ .kebab {
424
+ width: 34px;
425
+ height: 34px;
426
+ border-radius: 999px;
427
+ border: 1px solid var(--border);
428
+ background: rgba(255, 255, 255, 0.75);
429
+ cursor: pointer;
430
+ display: grid;
431
+ place-items: center;
432
+ transition: border-color 0.15s ease;
433
+ font-size: 18px;
434
+ line-height: 1;
435
+ color: rgba(15, 23, 42, 0.74);
436
+ }
437
+
438
+ .kebab:hover {
439
+ border-color: rgba(15, 23, 42, 0.18);
440
+ }
441
+
442
+ .kebab:focus {
443
+ outline: none;
444
+ box-shadow: 0 0 0 4px rgba(47, 107, 255, 0.18);
445
+ }
446
+
447
+ /* Responsive gallery grid */
448
+ @media (max-width: 1200px) {
449
+ .gallery-grid {
450
+ grid-template-columns: repeat(3, minmax(0, 1fr));
451
+ }
452
+ }
453
+
454
+ @media (max-width: 980px) {
455
+ .gallery-grid {
456
+ grid-template-columns: repeat(2, minmax(0, 1fr));
457
+ }
458
  }
459
 
460
+ @media (max-width: 680px) {
461
+ .gallery-grid {
462
+ grid-template-columns: 1fr;
463
+ }
464
  }
assets/example_dumbbell_chart.txt ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import plotly.graph_objects as go
2
+ import pandas as pd
3
+
4
+ # Sample data for dumbbell chart
5
+ countries = ['Country A', 'Country B', 'Country C', 'Country D', 'Country E']
6
+ year_1952 = [65, 68, 70, 72, 75]
7
+ year_2002 = [72, 76, 78, 80, 82]
8
+
9
+ # Prepare line coordinates for connecting dots
10
+ line_x = []
11
+ line_y = []
12
+ for i, country in enumerate(countries):
13
+ line_x.extend([year_1952[i], year_2002[i], None])
14
+ line_y.extend([country, country, None])
15
+
16
+ # Create dumbbell chart
17
+ fig = go.Figure(
18
+ data=[
19
+ # Add connecting lines
20
+ go.Scatter(
21
+ x=line_x,
22
+ y=line_y,
23
+ mode='markers+lines',
24
+ showlegend=False,
25
+ marker=dict(
26
+ symbol="arrow",
27
+ color="black",
28
+ size=16,
29
+ angleref="previous",
30
+ standoff=8
31
+ )
32
+ ),
33
+ # Add first year markers
34
+ go.Scatter(
35
+ x=year_1952,
36
+ y=countries,
37
+ mode='markers',
38
+ name='1952',
39
+ marker=dict(color='green', size=10)
40
+ ),
41
+ # Add second year markers
42
+ go.Scatter(
43
+ x=year_2002,
44
+ y=countries,
45
+ mode='markers',
46
+ name='2002',
47
+ marker=dict(color='blue', size=10)
48
+ ),
49
+ ]
50
+ )
51
+
52
+ # Update layout
53
+ fig.update_layout(
54
+ title='Comparison Between Two Years',
55
+ height=800,
56
+ plot_bgcolor='white',
57
+ legend_itemclick=False
58
+ )
59
+
60
+ # Show the figure
61
+ fig.show()
assets/example_polar_bar.txt ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import plotly.graph_objects as go
4
+
5
+ # Sample data for demonstration (full year 2024)
6
+ data = {
7
+ 'date': pd.date_range('2024-01-01', periods=365, freq='D'),
8
+ 'value': np.random.randint(50, 200, 365)
9
+ }
10
+ df = pd.DataFrame(data)
11
+
12
+ # Extract calendar components
13
+ df['month'] = df['date'].dt.month # 1..12
14
+ # Convert pandas weekday (Monday=0..Sunday=6) to Sun=0..Sat=6
15
+ df['weekday_sun0'] = (df['date'].dt.dayofweek + 1) % 7
16
+
17
+ # Aggregate values by month x weekday
18
+ agg = (
19
+ df.groupby(['month', 'weekday_sun0'], as_index=False)['value']
20
+ .sum()
21
+ .rename(columns={'value': 'total_value'})
22
+ )
23
+
24
+ # Ensure all 12x7 cells exist (fill missing with 0)
25
+ full = pd.MultiIndex.from_product(
26
+ [range(1, 13), range(0, 7)],
27
+ names=['month', 'weekday_sun0']
28
+ ).to_frame(index=False)
29
+ agg = full.merge(agg, on=['month', 'weekday_sun0'], how='left').fillna({'total_value': 0.0})
30
+
31
+ # Labels for months and weekdays
32
+ month_labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
33
+ weekday_labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
34
+
35
+ agg['month_name'] = agg['month'].map(lambda m: month_labels[m-1])
36
+ agg['weekday_name'] = agg['weekday_sun0'].map(lambda w: weekday_labels[w])
37
+
38
+ # Polar "cell" geometry
39
+ # Each month occupies a 30-degree sector (360/12 = 30)
40
+ month_width = 360 / 12
41
+ agg['theta'] = (agg['month'] - 1) * month_width # 0, 30, 60, ..., 330 (Jan at 0)
42
+ agg['width'] = month_width # sector width
43
+ agg['base'] = agg['weekday_sun0'] # ring start (0..6)
44
+ agg['r'] = 1 # ring thickness (each weekday is one ring)
45
+
46
+ # Bin values into 5 categories for color coding
47
+ s = agg['total_value'].astype(float)
48
+ nonzero = s[s > 0]
49
+
50
+ if nonzero.empty:
51
+ agg['bin'] = 'All zero'
52
+ bin_labels = ['All zero']
53
+ else:
54
+ # Quantile binning on non-zero values
55
+ binned_nz = pd.qcut(nonzero, q=5, duplicates='drop')
56
+ intervals = binned_nz.cat.categories
57
+ bin_labels = [f'{iv.left:,.0f}–{iv.right:,.0f}' for iv in intervals]
58
+
59
+ nz_labels = pd.Series(binned_nz.astype(str), index=nonzero.index)
60
+ interval_to_label = {str(iv): lbl for iv, lbl in zip(intervals, bin_labels)}
61
+ nz_labels = nz_labels.map(interval_to_label)
62
+
63
+ agg['bin'] = '0' # default for zeros
64
+ agg.loc[nonzero.index, 'bin'] = nz_labels.values
65
+ bin_labels = ['0'] + bin_labels
66
+
67
+ # Color palette (5 colors)
68
+ palette = ['#edf8fb', '#b2e2e2', '#66c2a4', '#2ca25f', '#006d2c']
69
+ unique_bins = [b for b in bin_labels if b in agg['bin'].unique()]
70
+ colors = palette[:max(1, len(unique_bins))]
71
+ color_map = dict(zip(unique_bins, colors))
72
+
73
+ # Build figure with one Barpolar trace per bin
74
+ fig = go.Figure()
75
+
76
+ for b in unique_bins:
77
+ sub = agg[agg['bin'] == b]
78
+ fig.add_trace(go.Barpolar(
79
+ theta=sub['theta'],
80
+ r=sub['r'],
81
+ base=sub['base'],
82
+ width=sub['width'],
83
+ name=b,
84
+ marker_color=color_map[b],
85
+ marker_line_width=0, # removes gaps between cells
86
+ hovertemplate=(
87
+ 'Month: %{customdata[0]}<br>'
88
+ 'Weekday: %{customdata[1]}<br>'
89
+ 'Value: %{customdata[2]:,.2f}<extra></extra>'
90
+ ),
91
+ customdata=np.stack([sub['month_name'], sub['weekday_name'], sub['total_value']], axis=1),
92
+ ))
93
+
94
+ # Radial ticks placed at ring centers (0.5..6.5)
95
+ tickvals = [i + 0.5 for i in range(7)]
96
+
97
+ fig.update_layout(
98
+ title='Circular Calendar View - Monthly Values by Weekday (2024)',
99
+ template='plotly_white',
100
+ margin=dict(l=40, r=40, t=70, b=40),
101
+ polar=dict(
102
+ angularaxis=dict(
103
+ direction='clockwise',
104
+ rotation=90, # puts theta=0 (Jan) at top
105
+ tickmode='array',
106
+ tickvals=[i * month_width for i in range(12)],
107
+ ticktext=month_labels,
108
+ ),
109
+ radialaxis=dict(
110
+ range=[0, 7],
111
+ tickmode='array',
112
+ tickvals=tickvals,
113
+ ticktext=weekday_labels, # Sun..Sat
114
+ showline=False,
115
+ gridcolor='rgba(0,0,0,0.12)',
116
+ ),
117
+ ),
118
+ legend_title_text='Value (binned)',
119
+ )
120
+
121
+ fig.show()
assets/example_polar_scatter.txt ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import plotly.express as px
4
+
5
+ # Sample data for demonstration (full year 2024)
6
+ data = {
7
+ 'date': pd.date_range('2024-01-01', periods=365, freq='D'),
8
+ 'value': np.random.randint(50, 200, 365)
9
+ }
10
+ df = pd.DataFrame(data)
11
+
12
+ # Extract calendar components
13
+ df['month'] = df['date'].dt.month # 1..12
14
+ # Convert pandas weekday (Monday=0..Sunday=6) to Sun=0..Sat=6
15
+ df['weekday_sun0'] = (df['date'].dt.dayofweek + 1) % 7
16
+
17
+ # Aggregate values by month x weekday
18
+ agg = (
19
+ df.groupby(['month', 'weekday_sun0'], as_index=False)['value']
20
+ .sum()
21
+ .rename(columns={'value': 'total_value'})
22
+ )
23
+
24
+ # Ensure all 12x7 cells exist (fill missing with 0)
25
+ full = pd.MultiIndex.from_product(
26
+ [range(1, 13), range(0, 7)],
27
+ names=['month', 'weekday_sun0']
28
+ ).to_frame(index=False)
29
+ agg = full.merge(agg, on=['month', 'weekday_sun0'], how='left').fillna({'total_value': 0.0})
30
+
31
+ # Labels for months and weekdays
32
+ month_labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
33
+ weekday_labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
34
+
35
+ agg['month_name'] = agg['month'].map(lambda m: month_labels[m-1])
36
+ agg['weekday_name'] = agg['weekday_sun0'].map(lambda w: weekday_labels[w])
37
+
38
+ # Rings: 1..7 (Sun=1 inner → Sat=7 outer)
39
+ agg['r'] = agg['weekday_sun0'] + 1
40
+
41
+ # Bubble size normalization (log1p compresses large values; then scale to pixel range)
42
+ max_marker_px = 42
43
+ min_marker_px = 6
44
+
45
+ s = agg['total_value'].to_numpy(dtype=float)
46
+ s_log = np.log1p(s)
47
+
48
+ if np.allclose(s_log.max(), s_log.min()):
49
+ agg['size_px'] = min_marker_px
50
+ else:
51
+ # Scale log values to [min_marker_px, max_marker_px]
52
+ scaled = (s_log - s_log.min()) / (s_log.max() - s_log.min())
53
+ agg['size_px'] = min_marker_px + scaled * (max_marker_px - min_marker_px)
54
+
55
+ # Sizeref for area sizing
56
+ sizeref = 2.0 * agg['size_px'].max() / (max_marker_px ** 2)
57
+
58
+ # Build polar scatter chart
59
+ fig = px.scatter_polar(
60
+ agg,
61
+ r='r',
62
+ theta='month_name',
63
+ size='size_px',
64
+ size_max=max_marker_px,
65
+ color='total_value',
66
+ color_continuous_scale='Viridis',
67
+ hover_data={
68
+ 'month_name': True,
69
+ 'weekday_name': True,
70
+ 'total_value': ':,.2f',
71
+ 'r': False,
72
+ 'size_px': False,
73
+ 'month': False,
74
+ 'weekday_sun0': False,
75
+ },
76
+ title='Circular Calendar View - Monthly Values by Weekday (2024)',
77
+ )
78
+
79
+ # Force area sizing behavior
80
+ fig.update_traces(marker=dict(sizemode='area', sizeref=sizeref, line=dict(width=0.6)))
81
+
82
+ # Clockwise months, start Jan at top
83
+ fig.update_layout(
84
+ polar=dict(
85
+ angularaxis=dict(
86
+ direction='clockwise',
87
+ rotation=90, # puts Jan at the top
88
+ ),
89
+ radialaxis=dict(
90
+ tickmode='array',
91
+ tickvals=list(range(1, 8)),
92
+ ticktext=weekday_labels, # Sun..Sat
93
+ range=[0.5, 7.5],
94
+ showline=False,
95
+ gridcolor='rgba(0,0,0,0.12)',
96
+ ),
97
+ ),
98
+ coloraxis_colorbar=dict(title='Value'),
99
+ template='plotly_white',
100
+ margin=dict(l=40, r=40, t=70, b=40),
101
+ height=800,
102
+ )
103
+
104
+ fig.show()
data/1976-2020_president.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f6cb9143a67bdbfa805810e069027f677f56a0765b00e10ea50b7d1a70e45bb5
3
- size 509865
 
 
 
 
data/ewf_appearances.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:448f2700a84da26347970f2d97f957b13b484c77112f988e1f7f0b58e4c3a66a
3
- size 481873
 
 
 
 
data/space_missions_data.csv DELETED
@@ -1,3 +0,0 @@
1
- version https://git-lfs.github.com/spec/v1
2
- oid sha256:f95e3afab4a0ea4050125d838381771d700a18e5ae759675b133f9933f858958
3
- size 137506
 
 
 
 
gallery_data.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gallery data configuration for ChaRtBot
3
+ Add new gallery items by appending to the GALLERY_DATA list
4
+ """
5
+
6
+ GALLERY_DATA = [
7
+ {
8
+ "image": "/assets/gallery/1_image.png",
9
+ "prompt": "Create an Animated Choropleth map of the USA displaying state-wise percentage of votes for DEMOCRAT in all the years.",
10
+ "csv_link": "/assets/gallery/datasets/1976-2020_president.csv",
11
+ "badge": "Animated choropleth"
12
+ },
13
+ {
14
+ "image": "/assets/gallery/2_image.png",
15
+ "prompt": "Create a Manhattan-style plot where purchases are grouped along the x-axis by Category, with each point representing an Order_Value. Alternate colours by Category so each group forms its own block, highlighting large spending spikes.",
16
+ "csv_link": "/assets/gallery/datasets/amazon-purchases-2021-sample-5k.csv",
17
+ "badge": "Manhattan"
18
+ },
19
+ {
20
+ "image": "/assets/gallery/3_image.png",
21
+ "prompt": "Create a sunburst chart showing the launch ecosystem hierarchy for the year 2005 Use Company as the first level, Rocket as the second level, Mission as the third level, and MissionStatus as the outer level.",
22
+ "csv_link": "/assets/gallery/datasets/space_missions_data.csv",
23
+ "badge": "Hierarchical"
24
+ },
25
+ {
26
+ "image": "/assets/gallery/4_image.png",
27
+ "prompt": "Create a Sankey diagram where the flow goes from Team → Match Result where: - Source nodes are team_name - Target nodes are result Aggregate the number of matches for each team–opponent–result flow. For readability, limit the visualization to teams with the highest total match counts.",
28
+ "csv_link": "/assets/gallery/datasets/ewf_appearances.csv",
29
+ "badge": "Flow"
30
+ },
31
+ {
32
+ "image": "/assets/gallery/5_image.png",
33
+ "prompt": "Create a calendar-like circular gold price chart for the year 2020, wherein going clock-wise on the chart will move from Jan -> Dec. Each month shall have a number of bars equal to the number of days in that month. The length of each bar will represent the price of gold on that day. Use a mustard colour for all the bars.",
34
+ "csv_link": "/assets/gallery/datasets/gold prices.csv",
35
+ "badge": "Circular"
36
+ },
37
+ {
38
+ "image": "/assets/gallery/6_image.png",
39
+ "prompt": "Depict the sum of the purchase amount per day of the week using a heatmap. The X-axis should represent the months, labeled with month names, and the Y-axis should represent the days of the week. Use colour to depict the purchase amounts, with the X-axis ordered chronologically by both days and months.",
40
+ "csv_link": "/assets/gallery/datasets/amazon-purchases-2021-sample-5k.csv",
41
+ "badge": "Heatmap"
42
+ },
43
+ {
44
+ "image": "/assets/gallery/7_image.png",
45
+ "prompt": "Create a total of four visualizations, (2 rows, 2 columns) which display: 1. Top 5 Categories by Quantity 2. Top 5 Categories by Price 3. Average Purchase Price per State (USA choropleth map) 4. State-wise Quantity Distribution (USA choropleth map)",
46
+ "csv_link": "/assets/gallery/datasets/amazon-purchases-2021-sample-5k.csv",
47
+ "badge": "Subplots"
48
+ },
49
+ {
50
+ "image": "/assets/gallery/8_image.png",
51
+ "prompt": "Create a candle-stick chart of gold prices in the year 2020, add a column named 'Ticker' and make sure all the values in this column are 'Gold'.",
52
+ "csv_link": "/assets/gallery/datasets/gold prices.csv",
53
+ "badge": "Financial"
54
+ },
55
+ {
56
+ "image": "/assets/gallery/9_image.png",
57
+ "prompt": "Create a bubble chart that visualizes rocket launch prices over the years Use Date on the x-axis and Price on the y-axis. Size each bubble by Price to reflect the relative cost of each mission. colour the bubbles by Company. Ensure bubbles are semi-transparent to avoid overlap issues. Add detailed hover tooltips showing Company, Rocket, Mission, Location, MissionStatus, and Price.",
58
+ "csv_link": "/assets/gallery/datasets/space_missions_data.csv",
59
+ "badge": "Bubble"
60
+ },
61
+ {
62
+ "image": "/assets/gallery/10_image.png",
63
+ "prompt": "Create an animated bubble chart where Categories are represented as bubbles that grow, shrink, and gently reposition over time based on total Order_Value, revealing how category importance changes month by month.",
64
+ "csv_link": "/assets/gallery/datasets/amazon-purchases-2021-sample-5k.csv",
65
+ "badge": "Animated bubbles"
66
+ },
67
+ {
68
+ "image": "/assets/gallery/11_image.png",
69
+ "prompt": "Create a calendar-like circular gold price chart for the year 2020, wherein going clock-wise on the chart will move from Jan -> Dec. Each month shall have a number of bars equal to the number of days in that month. Colour of each bar: Based on the price of gold on that day. Length of each bar: Equal to the day of the month.",
70
+ "csv_link": "/assets/gallery/datasets/gold prices.csv",
71
+ "badge": "Circular"
72
+ },
73
+ {
74
+ "image": "/assets/gallery/12_image.png",
75
+ "prompt": "Create a dumbbell chart showing the teams' scores over the years. Each team shall have only two data points in their dumbbells, the first point and the last. Each point must be a sum of goals scored by the team in the specific season. Use blue colour for the first point and red for the second point in the dumbbell. In the tooltips show the following information: - Team name - Score - Season - Year",
76
+ "csv_link": "/assets/gallery/datasets/ewf_appearances.csv",
77
+ "badge": "Dumbbell"
78
+ }
79
+ ]
pages/amazon_purchases_chartbot.py DELETED
@@ -1,97 +0,0 @@
1
- import dash
2
- from dash import callback, Input, Output, State, ctx, html
3
- import dash_mantine_components as dmc
4
-
5
- from utils import chartbot_dataset_layout, prompt, helpers
6
-
7
- dash.register_page(__name__, path='/chartbot/amazon-purchases', name="Amazon Purchases 2021 (Sample)", order=4)
8
-
9
- DATA_FILE_PATH = helpers.get_app_file_path("data", "amazon-purchases-2021-sample-5k.csv")
10
-
11
- BUTTON_PROMPT1_ID = "amazon-purchases-prompt1"
12
- BUTTON_PROMPT2_ID = "amazon-purchases-prompt2"
13
- SUBMIT_BUTTON_ID = "amazon-purchases-submit-button"
14
-
15
- STARTER_PROMPTS_DICTIONARY = {
16
- BUTTON_PROMPT1_ID: {
17
- "label": "Purchase amount per day of the week",
18
- "text": "Depict the sum of the purchase amount per day of the week using a heatmap. The X-axis should represent the months, labeled with month names, and the Y-axis should represent the days of the week. Use color to depict the purchase amounts, with the X-axis ordered chronologically by both days and months."
19
- },
20
- BUTTON_PROMPT2_ID: {
21
- "label": "General trends",
22
- "text": """Create a total of four visualizations, (2 rows, 2 columns) which display:
23
- 1. Top 5 Categories by Quantity
24
- 2. Top 5 Categories by Price
25
- 3. Average Purchase Price per State
26
- 4. State-wise Quantity Distribution
27
- """
28
- }
29
- }
30
-
31
- layout, df = chartbot_dataset_layout.chartbot_common(
32
- "Amazon Purchases 2021 (Sample)",
33
- DATA_FILE_PATH,
34
- {key: value["label"] for key, value in STARTER_PROMPTS_DICTIONARY.items()},
35
- "amazon-purchases-textarea",
36
- SUBMIT_BUTTON_ID,
37
- "amazon-purchases-chartbot-output",
38
- "amazon-purchases-python-content-output",
39
- "amazon-purchases-download-html",
40
- "amazon-purchases-html-buffer"
41
- )
42
-
43
- @callback(
44
- Output("amazon-purchases-textarea", "value"),
45
- Input("amazon-purchases-prompt1", "n_clicks"),
46
- Input("amazon-purchases-prompt2", "n_clicks")
47
- )
48
- def update_prompt1_in_textarea(n_clicks1, n_clicks2):
49
- if n_clicks1 or n_clicks2:
50
- button_clicked = ctx.triggered_id
51
- return STARTER_PROMPTS_DICTIONARY[button_clicked]["text"]
52
-
53
- @callback(
54
- Output(SUBMIT_BUTTON_ID, "loading", allow_duplicate=True),
55
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
56
- prevent_initial_call=True
57
- )
58
- def update_submit_loading(n_clicks):
59
- if n_clicks != None:
60
- return True
61
- else:
62
- return False
63
-
64
- @callback(
65
- Output("amazon-purchases-chartbot-output", "children"),
66
- Output("amazon-purchases-python-content-output", "children"),
67
- Output("amazon-purchases-download-html", "style"),
68
- Output("amazon-purchases-html-buffer", "data"),
69
- Output(SUBMIT_BUTTON_ID, "loading"),
70
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
71
- State("amazon-purchases-textarea", "value"),
72
- prevent_initial_call=True
73
- )
74
- def create_graph(_, user_prompt):
75
- try:
76
- df_5_rows = df.head(5)
77
- data_top5_csv_string = df_5_rows.to_csv(index=False)
78
-
79
- result_output = prompt.get_response(user_prompt, data_top5_csv_string, DATA_FILE_PATH)
80
-
81
- return helpers.display_response(result_output, DATA_FILE_PATH)
82
-
83
- except Exception as e:
84
- error_message = str(e)
85
- return html.Div([
86
- html.Br(),
87
- dmc.Alert(error_message, title="Error", color="red")
88
- ]), None, {"display": "none"}, None, False
89
-
90
- @callback(
91
- Output("amazon-purchases-download-html", "href"),
92
- Input("amazon-purchases-html-buffer", "data")
93
- )
94
- def download_html(encoded):
95
- if encoded:
96
- return f"data:text/html;base64,{encoded}"
97
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/chartbot.py DELETED
@@ -1,140 +0,0 @@
1
- import dash
2
- from dash_iconify import DashIconify
3
- import dash_mantine_components as dmc
4
- from dash import html, dcc, callback, Input, Output, State
5
-
6
- import pandas as pd
7
- from utils import prompt, helpers
8
-
9
- dash.register_page(__name__, path='/chartbot', name="ChaRtBot", order=6)
10
-
11
- UPLOAD_COMPONENT_STYLE = {
12
- 'width': '100%',
13
- 'height': '60px',
14
- 'lineHeight': '60px',
15
- 'borderColor': '#A5A5AE',
16
- 'borderWidth': '1px',
17
- 'borderStyle': 'dashed',
18
- 'borderRadius': '10px',
19
- 'textAlign': 'center',
20
- 'marginTop': '10px',
21
- 'fontFamily': 'Helvetica',
22
- 'fontWeight': 'bold',
23
- }
24
-
25
- layout = html.Div([
26
- dcc.Store(id="html-buffer"),
27
-
28
- dmc.Title("Upload your data", id="dataset", order=1, style={'fontFamily': 'Helvetica'}),
29
-
30
- dcc.Upload(
31
- id='upload-data',
32
- children=html.Div([
33
- DashIconify(icon="icon-park-outline:upload-two", color="#70707A", height=40),
34
- html.A("Select a file", style={"text-decoration": "none", "color": "#0092F3"}),
35
- " or drag and drop here"
36
- ]),
37
- style=UPLOAD_COMPONENT_STYLE,
38
- accept=".csv, .xls", multiple=True
39
- ),
40
-
41
- html.Br(),
42
-
43
- html.Div(id='output-grid'),
44
-
45
- dmc.Group([
46
- dmc.Textarea(placeholder="Type the prompt here ...", id="textarea", size="lg", w=1170),
47
- dmc.Button("Submit", id="submit-button", color="#E71316", className="float-end")
48
- ]),
49
-
50
- dmc.Text(id="chart", style={"scrollMarginTop": "60px"}),
51
- html.Div(id="chartbot-output"),
52
- html.A(
53
- dmc.Tooltip(
54
- multiline=True,
55
- w=200,
56
- label="""Download the visualization as an HTML file.
57
- The downloaded file can then be opened in a web browser to view the visualization.
58
- It can also be embedded in a SharePoint site and shared with others in your team.""",
59
- position="right",
60
- withArrow=True,
61
- arrowSize=6,
62
- children=[dmc.Button("Download as HTML", color="#54545C")],
63
- className="float-end"
64
- ),
65
- id="download-html",
66
- download="plotly_graph.html",
67
- style={"display": "none"}
68
- ),
69
- dmc.Text(id="python-code", style={"scrollMarginTop": "80px"}),
70
- dcc.Markdown(id="python-content-output")
71
- ])
72
-
73
- @callback(
74
- Output("output-grid", "children"),
75
- Input("upload-data", "contents"),
76
- State("upload-data", "filename")
77
- )
78
- def update_grid(list_of_contents, list_of_names):
79
- if list_of_contents is not None:
80
- children = [
81
- helpers.parse_contents(c, n) for c, n in
82
- zip(list_of_contents, list_of_names)
83
- ]
84
-
85
- return children
86
-
87
- @callback(
88
- Output("submit-button", "loading", allow_duplicate=True),
89
- Input("submit-button", "n_clicks"),
90
- prevent_initial_call=True
91
- )
92
- def update_submit_loading(n_clicks):
93
- if n_clicks != None:
94
- return True
95
- else:
96
- return False
97
-
98
- @callback(
99
- Output("chartbot-output", "children"),
100
- Output("python-content-output", "children"),
101
- Output("download-html", "style"),
102
- Output("html-buffer", "data"),
103
- Output("submit-button", "loading"),
104
- Input("submit-button", "n_clicks"),
105
- State("textarea", "value"),
106
- State("stored-data", "data"),
107
- State("stored-file-name", "data"),
108
- prevent_initial_call=True
109
- )
110
- def create_graph(n_clicks, user_prompt, file_data, file_name):
111
- if n_clicks is None:
112
- return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
113
-
114
- else:
115
- try:
116
- df = pd.DataFrame(file_data)
117
- df_5_rows = df.head(5)
118
- data_top5_csv_string = df_5_rows.to_csv(index=False)
119
-
120
- helpers.save_dataframe_to_current_path(df, file_name)
121
-
122
- result_output = prompt.get_response(user_prompt, data_top5_csv_string, file_name)
123
-
124
- return helpers.display_response(result_output, file_name)
125
-
126
- except Exception as e:
127
- error_message = str(e)
128
- return html.Div([
129
- html.Br(),
130
- dmc.Alert(error_message, title="Error", color="red")
131
- ]), None, {"display": "none"}, None, False
132
-
133
- @callback(
134
- Output("download-html", "href"),
135
- Input("html-buffer", "data")
136
- )
137
- def download_html(encoded):
138
- if encoded:
139
- return f"data:text/html;base64,{encoded}"
140
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/ewf_chartbot.py DELETED
@@ -1,95 +0,0 @@
1
- import dash
2
- from dash import callback, Input, Output, State, ctx, html
3
- import dash_mantine_components as dmc
4
-
5
- from utils import chartbot_dataset_layout, prompt, helpers
6
-
7
- dash.register_page(__name__, path='/chartbot/football', name="English Women's Football Matches (2011-2024)", order=3)
8
-
9
- DATA_FILE_PATH = helpers.get_app_file_path("data", "ewf_appearances.csv")
10
-
11
- BUTTON_PROMPT1_ID = "ewf-prompt1"
12
- BUTTON_PROMPT2_ID = "ewf-prompt2"
13
- SUBMIT_BUTTON_ID = "ewf-submit-button"
14
-
15
- STARTER_PROMPTS_DICTIONARY = {
16
- BUTTON_PROMPT1_ID: {
17
- "label": "Top 5 teams analysis",
18
- "text": """Create a total of three visualizations:
19
- 1. The top 5 teams with the most wins
20
- 2. The top 5 teams with the most points
21
- 3. The top 5 teams with the highest attendance"""
22
- },
23
- BUTTON_PROMPT2_ID: {
24
- "label": "Attendance trend",
25
- "text": "Scatter plot wherein the x-axis is team name, y-axis is opponent name and the size of each marker is the attendance"
26
- }
27
- }
28
-
29
- layout, df = chartbot_dataset_layout.chartbot_common(
30
- "English Women's Football Matches (2011 - 2024)",
31
- DATA_FILE_PATH,
32
- {key: value["label"] for key, value in STARTER_PROMPTS_DICTIONARY.items()},
33
- "ewf-textarea",
34
- SUBMIT_BUTTON_ID,
35
- "ewf-chartbot-output",
36
- "ewf-python-content-output",
37
- "ewf-download-html",
38
- "ewf-html-buffer"
39
- )
40
-
41
- @callback(
42
- Output("ewf-textarea", "value"),
43
- Input("ewf-prompt1", "n_clicks"),
44
- Input("ewf-prompt2", "n_clicks")
45
- )
46
- def update_prompt1_in_textarea(n_clicks1, n_clicks2):
47
- if n_clicks1 or n_clicks2:
48
- button_clicked = ctx.triggered_id
49
- return STARTER_PROMPTS_DICTIONARY[button_clicked]["text"]
50
-
51
- @callback(
52
- Output(SUBMIT_BUTTON_ID, "loading", allow_duplicate=True),
53
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
54
- prevent_initial_call=True
55
- )
56
- def update_submit_loading(n_clicks):
57
- if n_clicks != None:
58
- return True
59
- else:
60
- return False
61
-
62
- @callback(
63
- Output("ewf-chartbot-output", "children"),
64
- Output("ewf-python-content-output", "children"),
65
- Output("ewf-download-html", "style"),
66
- Output("ewf-html-buffer", "data"),
67
- Output(SUBMIT_BUTTON_ID, "loading"),
68
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
69
- State("ewf-textarea", "value"),
70
- prevent_initial_call=True
71
- )
72
- def create_graph(_, user_prompt):
73
- try:
74
- df_5_rows = df.head(5)
75
- data_top5_csv_string = df_5_rows.to_csv(index=False)
76
-
77
- result_output = prompt.get_response(user_prompt, data_top5_csv_string, DATA_FILE_PATH)
78
-
79
- return helpers.display_response(result_output, DATA_FILE_PATH)
80
-
81
- except Exception as e:
82
- error_message = str(e)
83
- return html.Div([
84
- html.Br(),
85
- dmc.Alert(error_message, title="Error", color="red")
86
- ]), None, {"display": "none"}, None, False
87
-
88
- @callback(
89
- Output("ewf-download-html", "href"),
90
- Input("ewf-html-buffer", "data")
91
- )
92
- def download_html(encoded):
93
- if encoded:
94
- return f"data:text/html;base64,{encoded}"
95
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/overview.py DELETED
@@ -1,36 +0,0 @@
1
- import dash
2
- from dash import html, dcc
3
- import dash_mantine_components as dmc
4
- import dash_bootstrap_components as dbc
5
-
6
- from utils import components
7
-
8
- dash.register_page(__name__, path='/', name='Overview', order=1)
9
-
10
- layout = dbc.Container([
11
- dbc.Col([
12
-
13
- dmc.Title("EmPower.AI - A cost-effective reporting ChaRtBot", className="app-title"),
14
- html.Hr(),
15
-
16
- html.Div([
17
- dcc.Markdown("### _**Talk to your data, and watch it come to life in charts!**_"),
18
- html.Br(),
19
- dmc.Text("EmPower.AI transforms the way you interact with data. It's a cost-effective reporting ChaRtBot. A ChaRtBot is like a ChatBot, wherein the user can upload a CSV file and enter a prompt, or ask a question. Unlinke a ChatBot which responds in plain text, this ChaRtBot will generate data visualizations.", size="xl"),
20
- dmc.Text("A simple PowerBI dashboard with clean data and few visuals can take 2-4 hours, a medium complexity dashboard which requires data transformations can take 4-8 hours, and a complex dashboard can take 1-3 days. With EmPower.AI, you can get the same results in a matter of seconds.", size="xl"),
21
- dmc.Text("No more sifting through spreadsheets — just ask, and ChaRtBot delivers charts tailored to your needs.", size="xl"),
22
- dmc.Text(["Plus, ", dmc.Mark("you can download and embed these charts as HTML in SharePoint, eliminating the need for expensive PowerBI premium subscriptions.", color="lime")], size="xl"),
23
- ]),
24
-
25
- html.Br(),
26
-
27
- dmc.Group([
28
- components.summary_card("Try the ChaRtBot", "Test the ChaRtBot with popular sample datasets or upload your own .csv or .xlsx data.", isButtonMenu=True),
29
-
30
- components.summary_card("Roadmap for EmPower.AI", "Check out the product roadmap for EmPower.AI and how it can help Thermo Fisher Scientific Inc."),
31
-
32
- components.summary_card("FAQs", "Check out the answers to some of the Frequently Asked Questions regarding EmPower.AI."),
33
-
34
- ], justify="center"),
35
- ])
36
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/space_missions_chartbot.py DELETED
@@ -1,95 +0,0 @@
1
- import dash
2
- from dash import callback, Input, Output, State, ctx, html
3
- import dash_mantine_components as dmc
4
-
5
- from utils import chartbot_dataset_layout, prompt, helpers
6
-
7
- dash.register_page(__name__, path='/chartbot/space-missions', name="Space Missions (2000-2022)", order=5)
8
-
9
- DATA_FILE_PATH = helpers.get_app_file_path("data", "space_missions_data.csv")
10
-
11
- BUTTON_PROMPT1_ID = "space-missions-prompt1"
12
- BUTTON_PROMPT2_ID = "space-missions-prompt2"
13
- SUBMIT_BUTTON_ID = "space-missions-submit-button"
14
-
15
- STARTER_PROMPTS_DICTIONARY = {
16
- BUTTON_PROMPT1_ID: {
17
- "label": "Top 5 companies analysis",
18
- "text": """Create a total of three visualizations which display:
19
- 1. Percentage of mission statuses
20
- 2. The top 5 companies with the most missions
21
- 3. The top 5 companies which have spent the most money on missions"""
22
- },
23
- BUTTON_PROMPT2_ID: {
24
- "label": "Correlation between Price and Mission status",
25
- "text": "Create a graph to visualize the correlation between the Price and Mission status columns in a heatmap. The visualization should be grouped by Company"
26
- }
27
- }
28
-
29
- layout, df = chartbot_dataset_layout.chartbot_common(
30
- "Space Missions (2000 - 2022)",
31
- DATA_FILE_PATH,
32
- {key: value["label"] for key, value in STARTER_PROMPTS_DICTIONARY.items()},
33
- "space-missions-textarea",
34
- SUBMIT_BUTTON_ID,
35
- "space-missions-chartbot-output",
36
- "space-missions-python-content-output",
37
- "space-missions-download-html",
38
- "space-missions-html-buffer"
39
- )
40
-
41
- @callback(
42
- Output("space-missions-textarea", "value"),
43
- Input("space-missions-prompt1", "n_clicks"),
44
- Input("space-missions-prompt2", "n_clicks")
45
- )
46
- def update_prompt1_in_textarea(n_clicks1, n_clicks2):
47
- if n_clicks1 or n_clicks2:
48
- button_clicked = ctx.triggered_id
49
- return STARTER_PROMPTS_DICTIONARY[button_clicked]["text"]
50
-
51
- @callback(
52
- Output(SUBMIT_BUTTON_ID, "loading", allow_duplicate=True),
53
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
54
- prevent_initial_call=True
55
- )
56
- def update_submit_loading(n_clicks):
57
- if n_clicks != None:
58
- return True
59
- else:
60
- return False
61
-
62
- @callback(
63
- Output("space-missions-chartbot-output", "children"),
64
- Output("space-missions-python-content-output", "children"),
65
- Output("space-missions-download-html", "style"),
66
- Output("space-missions-html-buffer", "data"),
67
- Output(SUBMIT_BUTTON_ID, "loading"),
68
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
69
- State("space-missions-textarea", "value"),
70
- prevent_initial_call=True
71
- )
72
- def create_graph(_, user_prompt):
73
- try:
74
- df_5_rows = df.head(5)
75
- data_top5_csv_string = df_5_rows.to_csv(index=False)
76
-
77
- result_output = prompt.get_response(user_prompt, data_top5_csv_string, DATA_FILE_PATH)
78
-
79
- return helpers.display_response(result_output, DATA_FILE_PATH)
80
-
81
- except Exception as e:
82
- error_message = str(e)
83
- return html.Div([
84
- html.Br(),
85
- dmc.Alert(error_message, title="Error", color="red")
86
- ]), None, {"display": "none"}, None, False
87
-
88
- @callback(
89
- Output("space-missions-download-html", "href"),
90
- Input("space-missions-html-buffer", "data")
91
- )
92
- def download_html(encoded):
93
- if encoded:
94
- return f"data:text/html;base64,{encoded}"
95
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pages/us_elections_chartbot.py DELETED
@@ -1,98 +0,0 @@
1
- import dash
2
- from dash import callback, Input, Output, State, ctx, html
3
- import dash_mantine_components as dmc
4
-
5
- from utils import chartbot_dataset_layout, prompt, helpers
6
-
7
- dash.register_page(__name__, path='/chartbot/us-elections', name="USA Presidential Elections (1976-2020)", order=2)
8
-
9
- DATA_FILE_PATH = helpers.get_app_file_path("data", "1976-2020_president.csv")
10
-
11
- BUTTON_PROMPT1_ID = "us-elections-prompt1"
12
- BUTTON_PROMPT2_ID = "us-elections-prompt2"
13
- BUTTON_PROMPT3_ID = "us-elections-prompt3"
14
- SUBMIT_BUTTON_ID = "us-elections-submit-button"
15
-
16
- STARTER_PROMPTS_DICTIONARY = {
17
- BUTTON_PROMPT1_ID: {
18
- "label": "Percentage of votes per state",
19
- "text": "Create a choropleth map of the USA which displays the percentage of votes per state for DEMOCRAT in all the years given in the dataset, by adding a year frame."
20
- },
21
- BUTTON_PROMPT2_ID: {
22
- "label": "Percentage of total votes",
23
- "text": "Display the percentage of total votes received by each candidate in the DEMOCRAT (blue) and REPUBLICAN (red) parties in the year 1980 in a pie chart."
24
- },
25
- BUTTON_PROMPT3_ID: {
26
- "label": "Who won in each state",
27
- "text": "Create a choropleth map of only the USA for the year 2008 which colours the states based on which candidate won in them, either Democrat (blue) or Republican (red). Add the candidate name in the tooltip."
28
- }
29
- }
30
-
31
- layout, df = chartbot_dataset_layout.chartbot_common(
32
- "USA Presidential Elections (1976 - 2020)",
33
- DATA_FILE_PATH,
34
- {key: value["label"] for key, value in STARTER_PROMPTS_DICTIONARY.items()},
35
- "us-elections-textarea",
36
- SUBMIT_BUTTON_ID,
37
- "us-elections-chartbot-output",
38
- "us-elections-python-content-output",
39
- "us-elections-download-html",
40
- "us-elections-html-buffer"
41
- )
42
-
43
- @callback(
44
- Output("us-elections-textarea", "value"),
45
- Input("us-elections-prompt1", "n_clicks"),
46
- Input("us-elections-prompt2", "n_clicks"),
47
- Input("us-elections-prompt3", "n_clicks")
48
- )
49
- def update_prompt1_in_textarea(n_clicks1, n_clicks2, n_clicks3):
50
- if n_clicks1 or n_clicks2 or n_clicks3:
51
- button_clicked = ctx.triggered_id
52
- return STARTER_PROMPTS_DICTIONARY[button_clicked]["text"]
53
-
54
- @callback(
55
- Output(SUBMIT_BUTTON_ID, "loading", allow_duplicate=True),
56
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
57
- prevent_initial_call=True
58
- )
59
- def update_submit_loading(n_clicks):
60
- if n_clicks != None:
61
- return True
62
- else:
63
- return False
64
-
65
- @callback(
66
- Output("us-elections-chartbot-output", "children"),
67
- Output("us-elections-python-content-output", "children"),
68
- Output("us-elections-download-html", "style"),
69
- Output("us-elections-html-buffer", "data"),
70
- Output(SUBMIT_BUTTON_ID, "loading"),
71
- Input(SUBMIT_BUTTON_ID, "n_clicks"),
72
- State("us-elections-textarea", "value"),
73
- prevent_initial_call=True
74
- )
75
- def create_graph(_, user_prompt):
76
- try:
77
- df_5_rows = df.head(5)
78
- data_top5_csv_string = df_5_rows.to_csv(index=False)
79
-
80
- result_output = prompt.get_response(user_prompt, data_top5_csv_string, DATA_FILE_PATH)
81
-
82
- return helpers.display_response(result_output, DATA_FILE_PATH)
83
-
84
- except Exception as e:
85
- error_message = str(e)
86
- return html.Div([
87
- html.Br(),
88
- dmc.Alert(error_message, title="Error", color="red")
89
- ]), None, {"display": "none"}, None, False
90
-
91
- @callback(
92
- Output("us-elections-download-html", "href"),
93
- Input("us-elections-html-buffer", "data")
94
- )
95
- def download_html(encoded):
96
- if encoded:
97
- return f"data:text/html;base64,{encoded}"
98
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.12.0
3
+ blinker==1.9.0
4
+ certifi==2026.1.4
5
+ charset-normalizer==3.4.4
6
+ click==8.3.1
7
+ dash==3.3.0
8
+ dash_ag_grid==32.3.4
9
+ dash_mantine_components==2.4.1
10
+ distro==1.9.0
11
+ Flask==3.1.2
12
+ groq==0.37.1
13
+ h11==0.16.0
14
+ httpcore==1.0.9
15
+ httpx==0.28.1
16
+ idna==3.11
17
+ importlib_metadata==8.7.1
18
+ itsdangerous==2.2.0
19
+ Jinja2==3.1.6
20
+ jsonpatch==1.33
21
+ jsonpointer==3.0.0
22
+ langchain==1.2.0
23
+ langchain-core==1.2.6
24
+ langchain-groq==1.1.1
25
+ langgraph==1.0.5
26
+ langgraph-checkpoint==3.0.1
27
+ langgraph-prebuilt==1.0.5
28
+ langgraph-sdk==0.3.1
29
+ langsmith==0.6.0
30
+ MarkupSafe==3.0.3
31
+ narwhals==2.14.0
32
+ nest-asyncio==1.6.0
33
+ numpy==2.4.0
34
+ orjson==3.11.5
35
+ ormsgpack==1.12.1
36
+ packaging==25.0
37
+ pandas==2.3.3
38
+ plotly==6.5.0
39
+ pydantic==2.12.5
40
+ pydantic_core==2.41.5
41
+ python-dateutil==2.9.0.post0
42
+ python-dotenv==1.2.1
43
+ pytz==2025.2
44
+ PyYAML==6.0.3
45
+ requests==2.32.5
46
+ requests-toolbelt==1.0.0
47
+ retrying==1.4.2
48
+ setuptools==80.9.0
49
+ six==1.17.0
50
+ sniffio==1.3.1
51
+ tenacity==9.1.2
52
+ typing-inspection==0.4.2
53
+ typing_extensions==4.15.0
54
+ tzdata==2025.3
55
+ urllib3==2.6.2
56
+ uuid_utils==0.12.0
57
+ Werkzeug==3.1.4
58
+ xxhash==3.6.0
59
+ zipp==3.23.0
60
+ zstandard==0.25.0
utils/chartbot_dataset_layout.py DELETED
@@ -1,78 +0,0 @@
1
- import pandas as pd
2
-
3
- from dash import html, dcc
4
- import dash_ag_grid as dag
5
- import dash_mantine_components as dmc
6
-
7
- from utils import components
8
-
9
- def chartbot_common(
10
- page_title: str,
11
- csv_file_path: str,
12
- starter_prompts: dict,
13
- prompt_textarea_id: str,
14
- submit_button_id: str,
15
- chartbot_output_id: str,
16
- python_content_id: str,
17
- download_html_button_id: str,
18
- html_buffer_id: str
19
- ) -> tuple:
20
- df = pd.read_csv(csv_file_path)
21
-
22
- starter_prompts_list = list()
23
-
24
- for key, value in starter_prompts.items():
25
- starter_prompts_list.append(components.button_with_prompt(key, value))
26
-
27
- layout = html.Div([
28
- dcc.Store(id=html_buffer_id),
29
-
30
- dmc.Title(page_title, id="dataset", order=1, style={'fontFamily': 'Helvetica'}),
31
-
32
- html.Br(),
33
-
34
- dag.AgGrid(
35
- rowData=df.to_dict("records"),
36
- columnDefs=[{"field": col} for col in df.columns],
37
- defaultColDef={"filter": True, "sortable": True, "resizable": True},
38
- style={'fontFamily': 'Helvetica'}
39
- ),
40
-
41
- dmc.Text("Choose a prompt to get started:", size="lg", fw=500, style={"textAlign": "center", "paddingTop": "25px", "paddingBottom": "15px"}),
42
-
43
- dmc.Group(starter_prompts_list, justify="center"),
44
-
45
- html.Br(),
46
-
47
- dmc.Group([
48
- dmc.Textarea(placeholder="Type the prompt here ...", id=prompt_textarea_id, size="lg", w=1170),
49
- dmc.Button("Submit", id=submit_button_id, color="#E71316", className="float-end")
50
- ]),
51
-
52
- dmc.Text(id="chart", style={"scrollMarginTop": "60px"}),
53
- html.Div(id=chartbot_output_id),
54
- html.A(
55
- dmc.Tooltip(
56
- multiline=True,
57
- w=200,
58
- label="""Download the visualization as an HTML file.
59
- The downloaded file can then be opened in a web browser to view the visualization.
60
- It can also be embedded in a SharePoint site and shared with others in your team.""",
61
- position="right",
62
- withArrow=True,
63
- arrowSize=6,
64
- children=[dmc.Button("Download as HTML", color="#54545C")],
65
- className="float-end"
66
- ),
67
- id=download_html_button_id,
68
- download="plotly_graph.html",
69
- style={"display": "none"}
70
- ),
71
- dmc.Text(id="python-code", style={"scrollMarginTop": "80px"}),
72
- dcc.Markdown(id=python_content_id)
73
- ])
74
-
75
- return layout, df
76
-
77
-
78
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/components.py DELETED
@@ -1,59 +0,0 @@
1
- from dash import html
2
- from dash_iconify import DashIconify
3
- import dash_mantine_components as dmc
4
-
5
- def aside_link(
6
- label: str,
7
- href: str,
8
- icon: str
9
- ) -> html.Div:
10
- return html.Div(
11
- dmc.Group([
12
- DashIconify(icon=icon, color="#A5A5AE", height=20),
13
- html.A(label, href=href, style={"text-decoration": "none", "color": "#A5A5AE", "font-weight": "bold"})
14
- ])
15
- )
16
-
17
- def summary_card(
18
- title: str,
19
- description: str,
20
- isButtonMenu: bool = False
21
- ) -> dmc.Card:
22
- if isButtonMenu:
23
- return dmc.Card([
24
- dmc.CardSection([
25
- dmc.Text(title, size="xl", fw=500),
26
- dmc.Divider(variant="solid"),
27
- dmc.Space(h="sm"),
28
- dmc.Text(description, size="sm"),
29
- dmc.Space(h="md"),
30
- dmc.Menu([
31
- dmc.MenuTarget(dmc.Button("Get Started", color="#E71316", className="float-end")),
32
- dmc.MenuDropdown([
33
- dmc.MenuItem("USA Presidential Elections (1976-2020)", href="/chartbot/us-elections"),
34
- dmc.MenuItem("English Women's Football Matches (2011-2024)", href="/chartbot/football"),
35
- dmc.MenuItem("Amazon Purchases 2021 (Sample)", href="/chartbot/amazon-purchases"),
36
- #dmc.MenuItem("Space Missions (2000-2022)", href="/chartbot/space-missions"),
37
- dmc.MenuItem("Upload your data", href="/chartbot")
38
- ])
39
- ])
40
- ], style={"margin": 10})
41
- ], withBorder=True, radius="md", w=320)
42
-
43
- else:
44
- return dmc.Card([
45
- dmc.CardSection([
46
- dmc.Text(title, size="xl", fw=500),
47
- dmc.Divider(variant="solid"),
48
- dmc.Space(h="sm"),
49
- dmc.Text(description, size="sm"),
50
- dmc.Space(h="md"),
51
- dmc.Button("Get Started", color="#E71316", className="float-end")
52
- ], style={"margin": 10})
53
- ], withBorder=True, radius="md", w=320)
54
-
55
- def button_with_prompt(
56
- identity: str,
57
- prompt: str
58
- ) -> dmc.Button:
59
- return dmc.Button(prompt, id=identity, color="gray", variant="outline", radius="md")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/helpers.py CHANGED
@@ -12,8 +12,15 @@ from base64 import b64encode
12
  # libraries to help with the Dash app, layout, and callbacks
13
  import dash_ag_grid as dag
14
 
 
 
 
15
  from utils import prompt
16
 
 
 
 
 
17
  # Function to get the path of a file in the app source code
18
  def get_app_file_path(directory_name: str, file_name: str) -> str:
19
  return os.path.join(os.path.dirname(__file__), "..", directory_name, file_name)
@@ -34,22 +41,23 @@ def get_fig_from_code(code, file_name):
34
  exec(code, {}, local_variables)
35
 
36
  except Exception as e:
37
- try:
38
- result_output = prompt.get_python_exception_response(code, str(e))
39
- return display_response(result_output, file_name)
40
- except Exception as api_error:
41
- # If the API call fails, raise the error to be handled by display_response
42
- raise api_error
43
 
44
  return local_variables["fig"]
45
 
46
  def display_response(response, file_name):
47
  try:
48
  code_block_match = re.search(r"```(?:[Pp]ython)?(.*?)```", response, re.DOTALL)
49
- #print(code_block_match)
50
 
51
  if code_block_match:
52
  code_block = code_block_match.group(1).strip()
 
 
 
 
 
53
  cleaned_code = re.sub(r'(?m)^\s*fig\.show\(\)\s*$', '', code_block)
54
 
55
  try:
@@ -60,23 +68,62 @@ def display_response(response, file_name):
60
  html_bytes = buffer.getvalue().encode()
61
  encoded = b64encode(html_bytes).decode()
62
 
63
- return dcc.Graph(figure=fig), response, {"display": "block"}, encoded, False
64
-
65
- except AttributeError as e:
66
- return html.Div([
67
- html.Br(),
68
- dmc.Alert("The code generated has errors, please try again.", title="Error", color="red")
69
- ]), None, {"display": "none"}, None, False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  else:
72
- return "", response, {"display": "none"}, None, False
73
 
74
  except Exception as e:
75
- # Handle API errors gracefully
76
- error_message = str(e)
 
 
77
  return html.Div([
78
  html.Br(),
79
- dmc.Alert(error_message, title="API Error", color="red")
 
 
 
 
80
  ]), None, {"display": "none"}, None, False
81
 
82
  # Function to parse the contents of the uploaded file
@@ -120,4 +167,40 @@ def save_dataframe_to_current_path(df: pd.DataFrame, filename: str) -> None:
120
  df.to_csv(filename, index=False)
121
 
122
  elif 'xls' in filename:
123
- df.to_excel(filename, index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  # libraries to help with the Dash app, layout, and callbacks
13
  import dash_ag_grid as dag
14
 
15
+ # Add logging import
16
+ import logging
17
+
18
  from utils import prompt
19
 
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.ERROR)
22
+ logger = logging.getLogger(__name__)
23
+
24
  # Function to get the path of a file in the app source code
25
  def get_app_file_path(directory_name: str, file_name: str) -> str:
26
  return os.path.join(os.path.dirname(__file__), "..", directory_name, file_name)
 
41
  exec(code, {}, local_variables)
42
 
43
  except Exception as e:
44
+ # Raise the exception to be handled by the caller
45
+ # Don't call display_response here as it would cause incorrect return value count
46
+ raise e
 
 
 
47
 
48
  return local_variables["fig"]
49
 
50
  def display_response(response, file_name):
51
  try:
52
  code_block_match = re.search(r"```(?:[Pp]ython)?(.*?)```", response, re.DOTALL)
 
53
 
54
  if code_block_match:
55
  code_block = code_block_match.group(1).strip()
56
+
57
+ # Check if code ends with fig.show() and add it if missing
58
+ if not re.search(r'fig\.show\(\)\s*$', code_block, re.MULTILINE):
59
+ code_block = code_block + "\nfig.show()"
60
+
61
  cleaned_code = re.sub(r'(?m)^\s*fig\.show\(\)\s*$', '', code_block)
62
 
63
  try:
 
68
  html_bytes = buffer.getvalue().encode()
69
  encoded = b64encode(html_bytes).decode()
70
 
71
+ return dcc.Graph(figure=fig), None, {"display": "block"}, encoded, False
72
+
73
+ except Exception as e:
74
+ # Log the original error
75
+ logger.error(f"Code execution error for file '{file_name}': {str(e)}", exc_info=True)
76
+
77
+ # Try to get corrected code from LLM
78
+ try:
79
+ result_output = prompt.get_python_exception_response(cleaned_code, str(e))
80
+ # Parse the corrected code
81
+ corrected_code_match = re.search(r"```(?:[Pp]ython)?(.*?)```", result_output, re.DOTALL)
82
+ if corrected_code_match:
83
+ corrected_code = corrected_code_match.group(1).strip()
84
+ corrected_code_clean = re.sub(r'(?m)^\s*fig\.show\(\)\s*$', '', corrected_code)
85
+
86
+ # Try to execute corrected code
87
+ fig = get_fig_from_code(corrected_code_clean, file_name)
88
+
89
+ buffer = io.StringIO()
90
+ fig.write_html(buffer)
91
+ html_bytes = buffer.getvalue().encode()
92
+ encoded = b64encode(html_bytes).decode()
93
+
94
+ return dcc.Graph(figure=fig), None, {"display": "block"}, encoded, False
95
+ else:
96
+ raise ValueError("No code block found in corrected response")
97
+
98
+ except Exception as api_error:
99
+ # Log the retry error
100
+ logger.error(f"Retry failed for file '{file_name}': {str(api_error)}", exc_info=True)
101
+
102
+ # Show user-friendly error message
103
+ return html.Div([
104
+ html.Br(),
105
+ dmc.Alert(
106
+ "We couldn't process your request. Please try modifying your prompt or check your data format.",
107
+ title="Unable to Generate Chart",
108
+ color="red"
109
+ )
110
+ ]), None, {"display": "none"}, None, False
111
 
112
  else:
113
+ return "", None, {"display": "none"}, None, False
114
 
115
  except Exception as e:
116
+ # Log API errors
117
+ logger.error(f"API error: {str(e)}", exc_info=True)
118
+
119
+ # Handle API errors gracefully with user-friendly message
120
  return html.Div([
121
  html.Br(),
122
+ dmc.Alert(
123
+ "We couldn't process your request. Please wait a moment and try again.",
124
+ title="Service Error",
125
+ color="red"
126
+ )
127
  ]), None, {"display": "none"}, None, False
128
 
129
  # Function to parse the contents of the uploaded file
 
167
  df.to_csv(filename, index=False)
168
 
169
  elif 'xls' in filename:
170
+ df.to_excel(filename, index=False)
171
+
172
+ def create_ag_grid(df):
173
+ """
174
+ Create a Dash AG Grid component for data exploration.
175
+
176
+ Args:
177
+ df: pandas DataFrame
178
+
179
+ Returns:
180
+ dag.AgGrid component
181
+ """
182
+ return dag.AgGrid(
183
+ id="data-explorer-grid",
184
+ rowData=df.to_dict("records"),
185
+ columnDefs=[{
186
+ "field": col,
187
+ "filter": True,
188
+ "sortable": True,
189
+ "resizable": True,
190
+ "floatingFilter": True
191
+ } for col in df.columns],
192
+ defaultColDef={
193
+ "filter": True,
194
+ "sortable": True,
195
+ "resizable": True,
196
+ "minWidth": 100
197
+ },
198
+ dashGridOptions={
199
+ "pagination": True,
200
+ "paginationPageSize": 10,
201
+ "paginationPageSizeSelector": [10, 20, 50, 100],
202
+ "animateRows": True
203
+ },
204
+ style={"height": "400px", "width": "100%"},
205
+ className="ag-theme-alpine"
206
+ )
utils/prompt.py CHANGED
@@ -1,6 +1,7 @@
1
  # libraries to help with the environment variables
2
  import os
3
  from dotenv import load_dotenv
 
4
 
5
  # libraries to help with the AI model
6
  from langchain_groq import ChatGroq
@@ -9,6 +10,10 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
9
 
10
  from utils import helpers
11
 
 
 
 
 
12
  # get the credentials from .env
13
  load_dotenv()
14
  GROQ_API_KEY = os.getenv('GROQ_API_KEY')
@@ -23,19 +28,47 @@ if not GROQ_API_KEY or GROQ_API_KEY == 'your_groq_api_key_here':
23
  # define connectivity to the llm
24
  try:
25
  llm = ChatGroq(
26
- model="groq/compound-mini",
27
  api_key=GROQ_API_KEY,
28
  temperature=0
29
  )
30
  except Exception as e:
31
  raise ValueError(f"Failed to initialize ChatGroq: {str(e)}")
32
 
33
- '''Before creating any visualizations, ensure that any rows with NaN or missing values in the relevant columns are removed. Additionally,
34
- handle missing values appropriately based on the context, ensuring cleaner visualizations.
35
- For example, use df.dropna(subset=[column_name]) for data cleaning. Never use this statement: df.dropna(inplace=True).'''
36
-
37
  def get_prompt_text() -> str:
38
- return """You are a data visualization expert and you only use the graphing library Plotly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  Ensure that before performing any data manipulation or plotting, the code checks for column data types and converts them if necessary.
40
  For example, numeric columns should be converted to floats or integers using pd.to_numeric(), and non-numeric columns should be excluded from numeric operations.
41
  Before creating any visualizations, ensure that any rows with NaN or missing values in the relevant columns are removed. Additionally,
@@ -44,11 +77,44 @@ def get_prompt_text() -> str:
44
  The graphs you plot shall always have a white background and shall follow data visualization best practices.
45
  Do not ignore any of the following visualization best practices:
46
  {data_visualization_best_practices}
47
- If the user requests a single visualization, create the graph using the plotly.express library and set the fig height to 800.
48
  Ensure that the graph is clearly labeled with a title, x-axis label, y-axis label, and legend.
49
- If the user has requested for a choropleth map of the United States of America (USA), ensure that the locations parameter in the px.choropleth() method is
50
- set to the column which contains the two letter code state abbreviations, for example: AL, NY, TN, VT, UT (the column should not be determined by the name of the column,
51
- but by the values it contains) and the scope parameter is set to 'usa'.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  If the user requests multiple visualizations, create a subplot for each visualization.
53
  The libraries required for multiple visualizations are: import plotly.graph_objects as go and from plotly.subplots import make_subplots.
54
  Utilize the plotly.graph_objects library's make_subplots() method to create subplots, specifying the number of rows and columns,
@@ -61,7 +127,6 @@ def get_prompt_text() -> str:
61
  particularly ensuring that pie charts are in domain-type subplots. If an error is detected, correct the subplot type automatically.
62
  Validate the layout before adding traces.
63
  Ensure each subplot is clearly labeled and formatted according to best practices.
64
- All the labels in the graph should be of the font family Helvetica, be it title, x-axis, y-axis, or legend.
65
  Here are examples of how to create multiple visualizations in a single figure:
66
  Example 1: \n
67
  {example_subplots1}
@@ -72,7 +137,44 @@ def get_prompt_text() -> str:
72
  The height of the figure (fig) should be set to 800.
73
  Suppose that the data is provided as a {name_of_file} file.
74
  Here are the first 5 rows of the data set: {data}. Follow the user's indications when creating the graph.
75
- There should be no natural language text in the python code block."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  def get_response(user_input: str, data_top5_csv_string: str, file_name: str) -> str:
78
  """
@@ -84,50 +186,130 @@ def get_response(user_input: str, data_top5_csv_string: str, file_name: str) ->
84
  file_name: Name of the data file
85
 
86
  Returns:
87
- LLM response content
88
 
89
  Raises:
90
- Exception: If API call fails
91
  """
92
  try:
93
- prompt = ChatPromptTemplate.from_messages(
94
- [
95
- (
96
- "system",
97
- get_prompt_text()
98
- ),
99
- MessagesPlaceholder(variable_name="messages")
100
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  chain = prompt | llm
104
 
105
- response = chain.invoke(
106
- {
107
- "messages": [HumanMessage(content=user_input)],
108
- "data_visualization_best_practices": helpers.read_doc(helpers.get_app_file_path("assets", "data_viz_best_practices.txt")),
109
- "example_subplots1": helpers.read_doc(helpers.get_app_file_path("assets", "example_subplots1.txt")),
110
- "example_subplots2": helpers.read_doc(helpers.get_app_file_path("assets", "example_subplots2.txt")),
111
- "example_subplots3": helpers.read_doc(helpers.get_app_file_path("assets", "example_subplots3.txt")),
112
- "data": data_top5_csv_string,
113
- "name_of_file": file_name
114
- }
115
- )
 
 
 
 
 
 
 
 
116
 
117
- return response.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  except Exception as e:
120
  error_msg = str(e)
 
 
 
 
 
 
121
  if "rate_limit" in error_msg.lower() or "429" in error_msg:
122
  raise Exception("Rate limit exceeded. Please wait a moment and try again.")
123
  elif "authentication" in error_msg.lower() or "401" in error_msg or "api_key" in error_msg.lower():
124
- raise Exception("Authentication failed. Please check your GROQ_API_KEY in the .env file.")
125
  elif "timeout" in error_msg.lower():
126
  raise Exception("Request timed out. Please try again.")
127
  else:
128
- raise Exception(f"Error communicating with Groq API: {error_msg}")
129
 
130
  def get_python_exception_prompt_text() -> str:
 
 
 
 
 
 
131
  return """The Python code you provided {code} has an error {exception}"""
132
 
133
  def get_python_exception_response(code: str, exception: str) -> str:
@@ -146,20 +328,20 @@ def get_python_exception_response(code: str, exception: str) -> str:
146
  """
147
  try:
148
  prompt = ChatPromptTemplate.from_messages(
149
- [
150
- (
151
- "system",
152
- get_python_exception_prompt_text()
153
- ),
154
- MessagesPlaceholder(variable_name="messages")
155
- ]
156
- )
157
 
158
  chain = prompt | llm
159
 
160
  response = chain.invoke(
161
  {
162
- "messages": [HumanMessage(content="Rewrite the entire Python code so that it does not contain any errors. The code should be able to run without any errors.")],
 
 
 
163
  "code": code,
164
  "exception": exception
165
  }
@@ -169,11 +351,16 @@ def get_python_exception_response(code: str, exception: str) -> str:
169
 
170
  except Exception as e:
171
  error_msg = str(e)
 
 
 
 
 
172
  if "rate_limit" in error_msg.lower() or "429" in error_msg:
173
  raise Exception("Rate limit exceeded. Please wait a moment and try again.")
174
  elif "authentication" in error_msg.lower() or "401" in error_msg or "api_key" in error_msg.lower():
175
- raise Exception("Authentication failed. Please check your GROQ_API_KEY in the .env file.")
176
  elif "timeout" in error_msg.lower():
177
  raise Exception("Request timed out. Please try again.")
178
  else:
179
- raise Exception(f"Error communicating with Groq API: {error_msg}")
 
1
  # libraries to help with the environment variables
2
  import os
3
  from dotenv import load_dotenv
4
+ import logging
5
 
6
  # libraries to help with the AI model
7
  from langchain_groq import ChatGroq
 
10
 
11
  from utils import helpers
12
 
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
  # get the credentials from .env
18
  load_dotenv()
19
  GROQ_API_KEY = os.getenv('GROQ_API_KEY')
 
28
  # define connectivity to the llm
29
  try:
30
  llm = ChatGroq(
31
+ model="llama-3.3-70b-versatile",
32
  api_key=GROQ_API_KEY,
33
  temperature=0
34
  )
35
  except Exception as e:
36
  raise ValueError(f"Failed to initialize ChatGroq: {str(e)}")
37
 
 
 
 
 
38
  def get_prompt_text() -> str:
39
+ """
40
+ Get the system prompt for data visualization generation.
41
+
42
+ Returns:
43
+ str: The system prompt template
44
+ """
45
+ return """You are a data visualization expert and you only use the graphing library Plotly.
46
+
47
+ CRITICAL VALIDATION RULES - EXECUTE BEFORE GENERATING ANY CODE:
48
+ 1. RELEVANCE CHECK: Before generating any code, you MUST verify that the user's request is relevant to the provided dataset.
49
+ 2. COLUMN VERIFICATION: Analyze the first 5 rows of data provided. If the user explicitly mentions column names that do NOT exist in the dataset, you MUST return an error message instead of code.
50
+ 3. DATA CONTEXT VERIFICATION: If the user's request asks about metrics, categories, or data points that are clearly incompatible with the dataset columns shown, you MUST return an error message instead of code.
51
+ 4. NON-VISUALIZATION REQUESTS: If the user's request is not about data visualization (e.g., asking for text generation, general questions, unrelated tasks), you MUST return an error message instead of code.
52
+
53
+ ERROR MESSAGE FORMAT - Use this EXACT format when validation fails:
54
+ ERROR: The request appears to be unrelated to the provided dataset. Please rephrase your request to refer to the actual columns and data available in your file. Available columns are: [list the column names from the data provided].
55
+
56
+ IMPORTANT: Only generate Python code if ALL of the following are true:
57
+ - The request is about creating a data visualization
58
+ - The request refers to columns, metrics, or patterns that could reasonably exist in the provided dataset
59
+ - The user has not explicitly mentioned column names that don't exist in the dataset
60
+
61
+ If any validation rule fails, return ONLY the error message in the format specified above. Do NOT generate any Python code.
62
+
63
+ IF VALIDATION PASSES, PROCEED WITH CODE GENERATION:
64
+
65
+ PANDAS DATA HANDLING BEST PRACTICES:
66
+ - Always use .copy() when creating a new dataframe from a subset or filtered view to avoid SettingWithCopyWarning.
67
+ - Example: df_filtered = df[df['column'] > 0].copy()
68
+ - When modifying data, always work on explicit copies, not chained indexing.
69
+ - Use .loc[] for setting values: df.loc[condition, 'column'] = value
70
+ - Avoid chained assignment like df[condition]['column'] = value
71
+
72
  Ensure that before performing any data manipulation or plotting, the code checks for column data types and converts them if necessary.
73
  For example, numeric columns should be converted to floats or integers using pd.to_numeric(), and non-numeric columns should be excluded from numeric operations.
74
  Before creating any visualizations, ensure that any rows with NaN or missing values in the relevant columns are removed. Additionally,
 
77
  The graphs you plot shall always have a white background and shall follow data visualization best practices.
78
  Do not ignore any of the following visualization best practices:
79
  {data_visualization_best_practices}
80
+ If the user requests a single visualization, figure height to 800.
81
  Ensure that the graph is clearly labeled with a title, x-axis label, y-axis label, and legend.
82
+
83
+ SPECIFIC CHART TYPE INSTRUCTIONS:
84
+
85
+ CHOROPLETH MAPS:
86
+ CRITICAL: When creating a choropleth map of the United States, you MUST include ALL of the following parameters:
87
+ - locations: Set to the column containing two-letter state abbreviations (e.g., 'AL', 'NY', 'CA', 'TX')
88
+ - locationmode: MUST be set to 'USA-states' (this is CRITICAL - without it, the map will be blank)
89
+ - scope: Set to 'usa'
90
+ Example:
91
+ fig = px.choropleth(df,
92
+ locations='state_code_column',
93
+ locationmode='USA-states',
94
+ scope='usa',
95
+ color='value_column',
96
+ title='Map Title')
97
+ The locations parameter should reference the column with state codes, not the column with full state names.
98
+ Always verify that locationmode='USA-states' is present in the code.
99
+
100
+ CHOROPLETH MAPS IN SUBPLOTS:
101
+ When creating a choropleth map of the USA as part of subplots using graph_objects (go.Choropleth), you MUST include the following:
102
+ 1. After creating all traces with fig.add_trace(), add:
103
+ fig.update_layout(title_text='Your Title', height=800, plot_bgcolor='white', paper_bgcolor='white')
104
+ Note: Generic background settings don't apply to choropleths in subplots, so this is necessary.
105
+ 2. Force the USA scope on the specific choropleth subplot using:
106
+ fig.update_geos(scope="usa", projection_type="albers usa", bgcolor="white", row=X, col=Y)
107
+ Replace X and Y with the actual row and column numbers where the choropleth is located.
108
+ Example:
109
+ # After adding all traces
110
+ fig.update_layout(title_text='E-commerce Data Visualization', height=800, plot_bgcolor='white', paper_bgcolor='white')
111
+ # Force USA scope on the choropleth subplot (adjust row, col as needed)
112
+ fig.update_geos(scope="usa", projection_type="albers usa", bgcolor="white", row=2, col=2)
113
+
114
+ {dumbbell_charts_section}
115
+
116
+ {polar_charts_section}
117
+
118
  If the user requests multiple visualizations, create a subplot for each visualization.
119
  The libraries required for multiple visualizations are: import plotly.graph_objects as go and from plotly.subplots import make_subplots.
120
  Utilize the plotly.graph_objects library's make_subplots() method to create subplots, specifying the number of rows and columns,
 
127
  particularly ensuring that pie charts are in domain-type subplots. If an error is detected, correct the subplot type automatically.
128
  Validate the layout before adding traces.
129
  Ensure each subplot is clearly labeled and formatted according to best practices.
 
130
  Here are examples of how to create multiple visualizations in a single figure:
131
  Example 1: \n
132
  {example_subplots1}
 
137
  The height of the figure (fig) should be set to 800.
138
  Suppose that the data is provided as a {name_of_file} file.
139
  Here are the first 5 rows of the data set: {data}. Follow the user's indications when creating the graph.
140
+ There should be no natural language text in the python code block.
141
+
142
+ REMINDER: Your code MUST end with fig.show() to display the visualization."""
143
+
144
+ def _should_include_dumbbell_examples(user_input: str) -> bool:
145
+ """
146
+ Check if user's request is about dumbbell charts or comparison visualizations.
147
+
148
+ Args:
149
+ user_input: User's visualization request
150
+
151
+ Returns:
152
+ bool: True if dumbbell chart examples should be included
153
+ """
154
+ dumbbell_keywords = [
155
+ 'dumbbell', 'dumb bell', 'dumbell', 'dumbel', 'comparison', 'before and after', 'before after',
156
+ 'start and end', 'start end', 'range', 'difference', 'gap', 'change over'
157
+ ]
158
+
159
+ user_input_lower = user_input.lower()
160
+ return any(keyword in user_input_lower for keyword in dumbbell_keywords)
161
+
162
+ def _should_include_polar_examples(user_input: str) -> bool:
163
+ """
164
+ Check if user's request is about polar charts, calendar views, or circular visualizations.
165
+
166
+ Args:
167
+ user_input: User's visualization request
168
+
169
+ Returns:
170
+ bool: True if polar chart examples should be included
171
+ """
172
+ polar_keywords = [
173
+ 'polar', 'circular', 'radial', 'circular fashion', 'radar', 'rose'
174
+ ]
175
+
176
+ user_input_lower = user_input.lower()
177
+ return any(keyword in user_input_lower for keyword in polar_keywords)
178
 
179
  def get_response(user_input: str, data_top5_csv_string: str, file_name: str) -> str:
180
  """
 
186
  file_name: Name of the data file
187
 
188
  Returns:
189
+ LLM response content containing Python code or error message
190
 
191
  Raises:
192
+ Exception: If API call fails or validation fails
193
  """
194
  try:
195
+ # Determine if dumbbell chart examples should be included
196
+ include_dumbbell = _should_include_dumbbell_examples(user_input)
197
+
198
+ # Determine if polar chart examples should be included
199
+ include_polar = _should_include_polar_examples(user_input)
200
+
201
+ # Build dumbbell charts section conditionally
202
+ dumbbell_charts_section = ""
203
+ if include_dumbbell:
204
+ dumbbell_example = helpers.read_doc(
205
+ helpers.get_app_file_path("assets", "example_dumbbell_chart.txt")
206
+ )
207
+ dumbbell_charts_section = f"""
208
+ DUMBBELL PLOTS:
209
+ When creating a dumbbell plot, use plotly.graph_objects (go) instead of plotly.express (px).
210
+ Use go.Figure() and add two go.Scatter traces for the two data points, and a go.Scatter trace for the lines connecting them.
211
+ Ensure proper labeling of axes and title for clarity.
212
+ Example: \n
213
+ {dumbbell_example}
214
+ """
215
+
216
+ # Build polar charts section conditionally
217
+ polar_charts_section = ""
218
+ if include_polar:
219
+ polar_bar_example = helpers.read_doc(
220
+ helpers.get_app_file_path("assets", "example_polar_bar.txt")
221
+ )
222
+ polar_scatter_example = helpers.read_doc(
223
+ helpers.get_app_file_path("assets", "example_polar_scatter.txt")
224
  )
225
+ polar_charts_section = f"""
226
+ POLAR CHARTS (RADIAL/CIRCULAR VISUALIZATIONS):
227
+ Polar charts are effective for displaying calendar views, weekly patterns, or circular data distributions.
228
+ Use them for innovative visualizations of time-based or cyclical data.
229
+
230
+ Example 1 - Polar Calendar with Cells (Barpolar):
231
+ {polar_bar_example}
232
+
233
+ Example 2 - Polar Calendar with Scatter:
234
+ {polar_scatter_example}
235
+
236
+ Use polar charts when the user requests:
237
+ - Calendar-like views
238
+ - Weekly or cyclical patterns
239
+ - Circular representations of data
240
+ - Radial visualizations
241
+ """
242
+
243
+ prompt = ChatPromptTemplate.from_messages(
244
+ [
245
+ ("system", get_prompt_text()),
246
+ MessagesPlaceholder(variable_name="messages")
247
+ ]
248
+ )
249
 
250
  chain = prompt | llm
251
 
252
+ invoke_params = {
253
+ "messages": [HumanMessage(content=user_input)],
254
+ "data_visualization_best_practices": helpers.read_doc(
255
+ helpers.get_app_file_path("assets", "data_viz_best_practices.txt")
256
+ ),
257
+ "example_subplots1": helpers.read_doc(
258
+ helpers.get_app_file_path("assets", "example_subplots1.txt")
259
+ ),
260
+ "example_subplots2": helpers.read_doc(
261
+ helpers.get_app_file_path("assets", "example_subplots2.txt")
262
+ ),
263
+ "example_subplots3": helpers.read_doc(
264
+ helpers.get_app_file_path("assets", "example_subplots3.txt")
265
+ ),
266
+ "dumbbell_charts_section": dumbbell_charts_section,
267
+ "polar_charts_section": polar_charts_section,
268
+ "data": data_top5_csv_string,
269
+ "name_of_file": file_name
270
+ }
271
 
272
+ response = chain.invoke(invoke_params)
273
+
274
+ # Check if the response is an error message instead of code
275
+ response_text = response.content.strip()
276
+
277
+ if response_text.startswith("ERROR:"):
278
+ # Extract the error message and raise validation error
279
+ error_message = response_text.replace("ERROR:", "").strip()
280
+ raise ValueError(error_message)
281
+
282
+ return response_text
283
+
284
+ except ValueError as ve:
285
+ # This is our custom validation error from the LLM
286
+ # Re-raise with user-friendly message
287
+ raise Exception(f"Unable to process your request: {str(ve)}")
288
 
289
  except Exception as e:
290
  error_msg = str(e)
291
+
292
+ # DEBUG: Log the actual error to understand what's happening
293
+ logger.info(f"DEBUG - Caught exception type: {type(e).__name__}")
294
+ logger.info(f"DEBUG - Error message: {error_msg}")
295
+
296
+ # Check for specific API errors (these are real API issues, not validation errors)
297
  if "rate_limit" in error_msg.lower() or "429" in error_msg:
298
  raise Exception("Rate limit exceeded. Please wait a moment and try again.")
299
  elif "authentication" in error_msg.lower() or "401" in error_msg or "api_key" in error_msg.lower():
300
+ raise Exception("We're having trouble generating your visualization.")
301
  elif "timeout" in error_msg.lower():
302
  raise Exception("Request timed out. Please try again.")
303
  else:
304
+ raise Exception(f"Unable to process your request: {error_msg}")
305
 
306
  def get_python_exception_prompt_text() -> str:
307
+ """
308
+ Get the system prompt for fixing Python code errors.
309
+
310
+ Returns:
311
+ str: The system prompt for error fixing
312
+ """
313
  return """The Python code you provided {code} has an error {exception}"""
314
 
315
  def get_python_exception_response(code: str, exception: str) -> str:
 
328
  """
329
  try:
330
  prompt = ChatPromptTemplate.from_messages(
331
+ [
332
+ ("system", get_python_exception_prompt_text()),
333
+ MessagesPlaceholder(variable_name="messages")
334
+ ]
335
+ )
 
 
 
336
 
337
  chain = prompt | llm
338
 
339
  response = chain.invoke(
340
  {
341
+ "messages": [HumanMessage(
342
+ content="Rewrite the entire Python code so that it does not contain any errors. "
343
+ "The code should be able to run without any errors."
344
+ )],
345
  "code": code,
346
  "exception": exception
347
  }
 
351
 
352
  except Exception as e:
353
  error_msg = str(e)
354
+
355
+ # Log the complete error message
356
+ logger.info(f"Exception fixing failed - Exception type: {type(e).__name__}")
357
+ logger.info(f"Exception fixing failed - Error message: {error_msg}")
358
+
359
  if "rate_limit" in error_msg.lower() or "429" in error_msg:
360
  raise Exception("Rate limit exceeded. Please wait a moment and try again.")
361
  elif "authentication" in error_msg.lower() or "401" in error_msg or "api_key" in error_msg.lower():
362
+ raise Exception("We're having trouble generating your visualization.")
363
  elif "timeout" in error_msg.lower():
364
  raise Exception("Request timed out. Please try again.")
365
  else:
366
+ raise Exception(f"Unable to process your request: {error_msg}")