Spaces:
Sleeping
Sleeping
Merge pull request #1 from deepa-shalini-k/custom-dev
Browse files- .gitignore +10 -1
- README.md +60 -1
- app.py +726 -110
- assets/custom.css +461 -5
- assets/example_dumbbell_chart.txt +61 -0
- assets/example_polar_bar.txt +121 -0
- assets/example_polar_scatter.txt +104 -0
- data/1976-2020_president.csv +0 -3
- data/ewf_appearances.csv +0 -3
- data/space_missions_data.csv +0 -3
- gallery_data.py +79 -0
- pages/amazon_purchases_chartbot.py +0 -97
- pages/chartbot.py +0 -140
- pages/ewf_chartbot.py +0 -95
- pages/overview.py +0 -36
- pages/space_missions_chartbot.py +0 -95
- pages/us_elections_chartbot.py +0 -98
- requirements.txt +60 -0
- utils/chartbot_dataset_layout.py +0 -78
- utils/components.py +0 -59
- utils/helpers.py +102 -19
- utils/prompt.py +234 -47
.gitignore
CHANGED
|
@@ -8,4 +8,13 @@ __pycache__/
|
|
| 8 |
# ignore environment variables
|
| 9 |
.env
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 3 |
import dash_mantine_components as dmc
|
| 4 |
-
import
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
-
_dash_renderer._set_react_version("18.2.0")
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
[
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
],
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
justify={"sm": "center"},
|
| 28 |
)
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 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 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
| 72 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
|
|
|
| 98 |
@callback(
|
| 99 |
-
Output("
|
| 100 |
-
Input("
|
| 101 |
)
|
| 102 |
-
def
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
| 106 |
|
|
|
|
| 107 |
@callback(
|
| 108 |
-
Output("
|
| 109 |
-
Output("
|
| 110 |
-
Output("
|
| 111 |
-
Output("
|
| 112 |
-
|
| 113 |
-
Output("
|
| 114 |
-
Output("
|
| 115 |
-
Output("
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
)
|
| 118 |
-
def
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 38 |
-
|
| 39 |
-
|
| 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),
|
| 64 |
-
|
| 65 |
-
except
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
else:
|
| 72 |
-
return "",
|
| 73 |
|
| 74 |
except Exception as e:
|
| 75 |
-
#
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
return html.Div([
|
| 78 |
html.Br(),
|
| 79 |
-
dmc.Alert(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 48 |
Ensure that the graph is clearly labeled with a title, x-axis label, y-axis label, and legend.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
chain = prompt | llm
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 125 |
elif "timeout" in error_msg.lower():
|
| 126 |
raise Exception("Request timed out. Please try again.")
|
| 127 |
else:
|
| 128 |
-
raise Exception(f"
|
| 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 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
MessagesPlaceholder(variable_name="messages")
|
| 155 |
-
]
|
| 156 |
-
)
|
| 157 |
|
| 158 |
chain = prompt | llm
|
| 159 |
|
| 160 |
response = chain.invoke(
|
| 161 |
{
|
| 162 |
-
"messages": [HumanMessage(
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 176 |
elif "timeout" in error_msg.lower():
|
| 177 |
raise Exception("Request timed out. Please try again.")
|
| 178 |
else:
|
| 179 |
-
raise Exception(f"
|
|
|
|
| 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}")
|