Sales Data Explorer (CSV)
Upload a CSV of sales orders (date, region, product, amount). Explore insights with summary stat cards, quick charts (Summary, Top products, Monthly trend), and a natural-language Q&A chat for non-technical sales managers. Charts render inline; answers are concise and business-friendly.
Preview Mode
This is a preview with sample data. The template uses placeholders like
which will be replaced with actual agent data.
About This Template
Sales Data Explorer (CSV) is a browser-executable AI agent template built on AgentOp. It runs entirely in the browser using Python (via Pyodide) and can be deployed without a server — just download the generated HTML file and open it locally or host it anywhere.
Template Metadata
- Slug
- sales-data-explorer-csv
- Created By
- ozzo
- Created
- Jun 19, 2026
- Usage Count
- 0
Tags
Code Statistics
- HTML Lines
- 95
- CSS Lines
- 62
- JS Lines
- 252
- Python Lines
- 296
Source Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ agent_name }}</title>
<style>
{{ css_code }}
</style>
</head>
<body>
<header class="app-header" role="banner">
<div class="brand">
<div class="logo">📈</div>
<div class="titles">
<h1 class="app-title">{{ agent_name }}</h1>
<p class="app-subtitle">{{ description }}</p>
</div>
</div>
<div class="actions">
<button id="btn-summary" class="action" disabled>Summary</button>
<button id="btn-top-products" class="action" disabled>Top products</button>
<button id="btn-monthly-trend" class="action" disabled>Monthly trend</button>
</div>
</header>
<main class="layout" role="main">
<section class="left-panel" aria-label="Upload and Summary">
<div class="card upload-card">
<h2>Upload sales CSV</h2>
<p class="muted">Columns required: date, region, product, amount</p>
<label class="upload-drop" for="file-input">
<input id="file-input" type="file" accept=".csv" />
<span id="upload-cta">Drop or choose a .csv file</span>
</label>
<div id="upload-status" class="status muted"></div>
</div>
<div class="card stats-card">
<h2>Sales summary</h2>
<div id="summary-empty" class="empty">
<p>Upload a sales CSV to see key stats:</p>
<ul>
<li>Total revenue and orders</li>
<li>Best region and products</li>
<li>Date range and trends</li>
<li>Quick charts you can share</li>
</ul>
</div>
<div id="stats-grid" class="stats-grid hidden"></div>
</div>
<div id="model-hint" class="card hint hidden"></div>
</section>
<section class="center-panel" aria-label="Charts and Visuals">
<div class="card visuals-card">
<div class="visuals-header">
<h2 id="visuals-title">Sales visuals</h2>
</div>
<div id="chart-area" class="chart-area">
<div id="chart-empty" class="empty">
<p>No chart yet.</p>
<p class="muted">Use the fast-path buttons above to render a chart for your sales data.</p>
</div>
<div id="chart-canvas" class="chart-canvas hidden"></div>
</div>
</div>
</section>
<aside class="right-panel" aria-label="Sales Q&A Chat">
<div class="card chat-card">
<div class="chat-header">
<h2>Sales Q&A</h2>
<div class="chips" id="example-chips"></div>
</div>
<div id="results-container" class="chat-log" aria-live="polite"></div>
<form id="chat-form" class="chat-form" autocomplete="off">
<input id="chat-input" type="text" placeholder="Ask a sales question (e.g., Which region grew fastest last quarter?)" />
<button id="chat-send" type="submit" class="primary">Ask</button>
</form>
</div>
</aside>
</main>
<div id="overlay" class="overlay hidden" aria-hidden="true">
<div class="spinner" aria-label="Loading"></div>
<div class="overlay-text">Working on your sales data…</div>
</div>
<script>
{{ js_code }}
</script>
</body>
</html>
:root{--bg:#0b1020;--panel:#121934;--card:#151f44;--muted:#90a0c0;--text:#e6eeff;--accent:#5aa7ff;--accent-2:#7ed9a8;--warn:#ffb86b;--danger:#ff6b8b;--ring:0 0 0 3px rgba(90,167,255,.35);--radius:14px;--shadow:0 8px 28px rgba(0,0,0,.35);--mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--sans:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,"Helvetica Neue",Arial,"Apple Color Emoji","Segoe UI Emoji"}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:linear-gradient(180deg,#0b1020 0%,#0e1430 100%);color:var(--text);font-family:var(--sans)}
.app-header{position:relative;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 20px;background:rgba(18,25,52,.7);backdrop-filter:saturate(1.2) blur(6px);border-bottom:1px solid rgba(255,255,255,.06)}
.brand{display:flex;align-items:center;gap:12px}
.logo{font-size:28px}
.app-title{margin:0;font-size:18px;font-weight:700}
.app-subtitle{margin:2px 0 0 0;font-size:12px;color:var(--muted);max-width:720px}
.actions{display:flex;gap:8px;flex-wrap:wrap}
.action{background:#1b2b5e;border:1px solid rgba(255,255,255,.08);color:var(--text);padding:10px 14px;border-radius:10px;cursor:pointer;transition:.2s;box-shadow:inset 0 -2px 0 rgba(255,255,255,.03)}
.action:hover{background:#20336e}
.action:disabled{opacity:.45;cursor:not-allowed}
.layout{display:grid;grid-template-columns:320px 1fr 420px;gap:18px;padding:18px;align-items:start}
.left-panel,.center-panel,.right-panel{min-width:0}
.card{background:var(--card);border:1px solid rgba(255,255,255,.06);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px}
.muted{color:var(--muted)}
.upload-card .upload-drop{display:block;margin-top:8px;background:#0f1736;border:2px dashed rgba(255,255,255,.12);border-radius:12px;padding:18px;text-align:center;color:var(--muted);cursor:pointer}
.upload-card input[type=file]{display:none}
.status{margin-top:8px;font-size:12px}
.stats-card h2,.visuals-card h2,.chat-card h2{margin:2px 0 8px 0}
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.stat{background:#0f1736;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:12px}
.stat .label{color:var(--muted);font-size:12px}
.stat .value{font-size:18px;font-weight:700;margin-top:4px}
.best{color:var(--accent-2)}
.hint{font-size:12px;color:var(--muted)}
.visuals-header{display:flex;align-items:center;justify-content:space-between}
.chart-area{min-height:280px}
.chart-canvas img{max-width:100%;height:auto;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:#0f1736}
.chat-card{display:flex;flex-direction:column;height:calc(100vh - 160px);min-height:520px}
.chat-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.chips{display:flex;gap:8px;flex-wrap:wrap}
.chip{background:#10214b;border:1px solid rgba(255,255,255,.08);color:var(--text);padding:6px 10px;border-radius:999px;font-size:12px;cursor:pointer}
.chat-log{flex:1;overflow:auto;background:#0f1736;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:12px}
.msg{display:flex;gap:10px;margin:8px 0}
.msg .avatar{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:#1a2a5c}
.msg.user .avatar{background:#2a4b8f}
.msg .bubble{flex:1;background:#0e1a3c;border:1px solid rgba(255,255,255,.06);border-radius:12px;padding:10px;white-space:pre-wrap;word-wrap:break-word}
.msg.assistant .bubble{background:#0e1a30}
.chat-form{display:flex;gap:8px;margin-top:10px}
.chat-form input{flex:1;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:#0f1736;color:var(--text)}
.chat-form input:focus{outline:none;box-shadow:var(--ring)}
.chat-form .primary{background:var(--accent);color:#091326;border:none;border-radius:10px;padding:10px 14px;cursor:pointer;font-weight:700}
.chat-form .primary:disabled{opacity:.5;cursor:not-allowed}
.empty{opacity:.8;border:1px dashed rgba(255,255,255,.12);border-radius:12px;padding:12px}
.hidden{display:none !important}
.overlay{position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(9,13,30,.55);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;z-index:50}
.spinner{width:38px;height:38px;border-radius:50%;border:4px solid rgba(255,255,255,.15);border-top-color:var(--accent);animation:spin 1s linear infinite}
.overlay-text{color:var(--text);font-weight:600}
@keyframes spin{to{transform:rotate(360deg)}}
@media (max-width:1200px){.layout{grid-template-columns:1fr 1fr}.right-panel{grid-column:1/3}}
@media (max-width:768px){.layout{grid-template-columns:1fr;gap:12px}.chat-card{height:auto}}
(function(){
const $ = (sel, el=document) => el.querySelector(sel);
const $$ = (sel, el=document) => Array.from(el.querySelectorAll(sel));
const overlay = $('#overlay');
const fileInput = $('#file-input');
const uploadStatus = $('#upload-status');
const statsGrid = $('#stats-grid');
const summaryEmpty = $('#summary-empty');
const chartEmpty = $('#chart-empty');
const chartCanvas = $('#chart-canvas');
const visualsTitle = $('#visuals-title');
const btnSummary = $('#btn-summary');
const btnTopProducts = $('#btn-top-products');
const btnMonthlyTrend = $('#btn-monthly-trend');
const modelHint = $('#model-hint');
const chatForm = $('#chat-form');
const chatInput = $('#chat-input');
const chatSend = $('#chat-send');
const chatLog = $('#results-container');
const exampleChips = $('#example-chips');
const EXAMPLES = [
'Which region grew fastest last quarter?',
'Top 5 products by revenue year-to-date.',
'Show the monthly sales trend for this year.',
'Any anomalies in March compared to February?'
];
function escapeHtml(str){
if(!str && str !== 0) return '';
return String(str)
.replace(/&/g,'&')
.replace(/</g,'<')
.replace(/>/g,'>')
.replace(/\"/g,'"')
.replace(/'/g,''');
}
function showOverlay(show){
overlay.classList.toggle('hidden', !show);
overlay.setAttribute('aria-hidden', show ? 'false' : 'true');
chatSend.disabled = show || chatSend.disabled;
}
function addChip(text){
const b = document.createElement('button');
b.type = 'button';
b.className = 'chip';
b.textContent = text;
b.addEventListener('click', ()=>{
chatInput.value = text;
chatInput.focus();
});
exampleChips.appendChild(b);
}
function addMessage(type, content){
const row = document.createElement('div');
row.className = 'msg ' + type;
const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.textContent = type === 'user' ? '🧑💼' : '🤖';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.textContent = content; // safe: we do not inject HTML
row.appendChild(avatar); row.appendChild(bubble);
chatLog.appendChild(row);
chatLog.scrollTop = chatLog.scrollHeight;
return bubble;
}
window.addMessage = addMessage; // enable local streaming hook
function clearChart(){
chartCanvas.innerHTML = '';
chartCanvas.classList.add('hidden');
chartEmpty.classList.remove('hidden');
}
function renderChart(dataUri, title){
if(!dataUri){ clearChart(); return; }
chartCanvas.innerHTML = '';
const img = document.createElement('img');
img.alt = title || 'Sales chart';
img.src = dataUri;
chartCanvas.appendChild(img);
chartCanvas.classList.remove('hidden');
chartEmpty.classList.add('hidden');
if(title) visualsTitle.textContent = title;
}
function renderSummary(summary){
if(!summary){
statsGrid.classList.add('hidden');
summaryEmpty.classList.remove('hidden');
return;
}
const s = summary; // object
statsGrid.innerHTML = '';
const items = [
{label:'Total revenue', value: s.total_revenue_fmt},
{label:'Total orders', value: s.total_orders.toLocaleString()},
{label:'Avg order value', value: s.avg_order_value_fmt},
{label:'Date range', value: s.date_range},
{label:'Regions', value: s.regions_count.toString()},
{label:'Products', value: s.products_count.toString()},
{label:'Top region', value: s.top_region || '—', cls:'best'},
{label:'Last vs prev month', value: s.mom_change || '—'}
];
items.forEach(it=>{
const d = document.createElement('div');
d.className = 'stat';
const l = document.createElement('div'); l.className = 'label'; l.textContent = it.label;
const v = document.createElement('div'); v.className = 'value' + (it.cls?(' '+it.cls):''); v.textContent = it.value;
d.appendChild(l); d.appendChild(v);
statsGrid.appendChild(d);
});
statsGrid.classList.remove('hidden');
summaryEmpty.classList.add('hidden');
}
function setButtonsEnabled(enabled){
[btnSummary, btnTopProducts, btnMonthlyTrend].forEach(b=> b.disabled = !enabled);
}
async function ensurePyodideReady(){
if(window.pyodideReady){ await window.pyodideReady; return; }
await new Promise(resolve => {
if(document.readyState === 'complete'){
// wait for pyodide-ready event
}
const onReady = ()=>{ document.removeEventListener('pyodide-ready', onReady); resolve(); };
document.addEventListener('pyodide-ready', onReady);
});
}
function isLocalModelRequiredButMissing(){
// If local provider, a model selector is injected. Require a loaded model.
try{
if(window.agentManager && !window.agentManager.isLoaded){ return true; }
}catch(e){/* ignore */}
return false;
}
async function ingestCsvText(text){
window.pyodide.globals.set('csv_text', text);
const res = await window.pyodide.runPythonAsync('_ingest_csv(csv_text)');
const data = JSON.parse(res);
if(!data.success){ throw new Error(data.error || 'Failed to load CSV'); }
return data.summary;
}
async function doSummary(){
showOverlay(true);
try{
const res = await window.pyodide.runPythonAsync('get_summary_cards()');
const obj = JSON.parse(res);
renderSummary(obj);
visualsTitle.textContent = 'Sales visuals';
}catch(err){
console.error(err);
uploadStatus.textContent = 'Error: '+ (err.message || err.toString());
}finally{ showOverlay(false); }
}
async function doTopProducts(){
showOverlay(true);
try{
const uri = await window.pyodide.runPythonAsync('chart_top_products(5)');
renderChart(uri, 'Top 5 products by revenue');
}catch(err){ renderChart('', ''); uploadStatus.textContent = 'Error: '+(err.message||err); }
finally{ showOverlay(false); }
}
async function doMonthlyTrend(){
showOverlay(true);
try{
const uri = await window.pyodide.runPythonAsync('chart_monthly_trend()');
renderChart(uri, 'Monthly revenue trend');
}catch(err){ renderChart('', ''); uploadStatus.textContent = 'Error: '+(err.message||err); }
finally{ showOverlay(false); }
}
async function handleChatSubmit(e){
e.preventDefault();
const q = chatInput.value.trim();
if(!q) return;
if(isLocalModelRequiredButMissing()){
modelHint.classList.remove('hidden');
modelHint.textContent = 'Load a local model from the top bar to use Sales Q&A.';
return;
}
addMessage('user', q);
chatInput.value = '';
chatSend.disabled = true;
try{
window.pyodide.globals.set('user_query', q);
// Let the platform's default dispatcher handle tools+LLM
const out = await window.pyodide.runPythonAsync('await process_user_query(user_query)');
addMessage('assistant', escapeHtml(out));
}catch(err){
addMessage('assistant', 'Error: ' + (err.message || String(err)));
}finally{
chatSend.disabled = false;
}
}
function initChips(){ EXAMPLES.forEach(addChip); }
async function init(){
initChips();
// Initial model hint for local-only
if(isLocalModelRequiredButMissing()){
modelHint.classList.remove('hidden');
modelHint.textContent = 'Tip: Select and load a local model from the top bar to enable Q&A.';
}
fileInput.addEventListener('change', async (e)=>{
const file = e.target.files && e.target.files[0];
if(!file){ return; }
const name = file.name || 'file.csv';
uploadStatus.textContent = 'Reading '+name+'…';
try{
const text = await file.text();
showOverlay(true);
const summary = await ingestCsvText(text);
uploadStatus.textContent = 'Loaded '+ name + ' • ' + summary.total_orders.toLocaleString() + ' orders';
renderSummary(summary);
setButtonsEnabled(true);
}catch(err){
console.error(err);
uploadStatus.textContent = 'Error: ' + (err.message || String(err));
renderSummary(null);
setButtonsEnabled(false);
clearChart();
}finally{
showOverlay(false);
}
});
btnSummary.addEventListener('click', doSummary);
btnTopProducts.addEventListener('click', doTopProducts);
btnMonthlyTrend.addEventListener('click', doMonthlyTrend);
chatForm.addEventListener('submit', handleChatSubmit);
}
(async ()=>{
try{ await ensurePyodideReady(); }catch(e){ console.error('Pyodide wait failed', e); }
document.dispatchEvent(new Event('agent-ui-ready'));
init();
})();
})();
import json
import math
import io
import base64
from typing import Optional
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
# Runtime state
_STATE = {
'df': None,
'summary': None,
}
_MAX_CHARS = int('[[[MAX_CHARS|6000]]]')
def _truncate(s: str, max_chars: int = _MAX_CHARS) -> str:
if s is None:
return ''
if len(s) <= max_chars:
return s
return s[:max_chars] + f"\n... (truncated to {max_chars} chars)"
def _fmt_currency(x: float) -> str:
try:
return f"${x:,.0f}" if abs(x) >= 1000 else f"${x:,.2f}"
except Exception:
return str(x)
def _ensure_df() -> pd.DataFrame:
if _STATE['df'] is None:
raise ValueError('No dataset loaded. Upload a CSV first.')
return _STATE['df']
def _parse_amount_col(df: pd.DataFrame) -> pd.DataFrame:
# unify amount column
cols = {c.lower().strip(): c for c in df.columns}
cand = None
for k in ['amount', 'revenue', 'sales', 'total', 'value']: # fallbacks
if k in cols:
cand = cols[k]
break
if cand is None:
raise ValueError('Missing amount/revenue column.')
df = df.rename(columns={cand: 'amount'})
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
df = df.dropna(subset=['amount'])
return df
def _standardize_columns(df: pd.DataFrame) -> pd.DataFrame:
cols = {c.lower().strip(): c for c in df.columns}
# date
dcol = None
for k in ['date', 'order_date', 'orderdate', 'dt']:
if k in cols:
dcol = cols[k]
break
if dcol is None:
raise ValueError('Missing date column.')
df = df.rename(columns={dcol: 'date'})
# region
rcol = None
for k in ['region', 'market', 'area']:
if k in cols:
rcol = cols[k]
break
if rcol is None:
raise ValueError('Missing region column.')
df = df.rename(columns={rcol: 'region'})
# product
pcol = None
for k in ['product', 'sku', 'item']:
if k in cols:
pcol = cols[k]
break
if pcol is None:
raise ValueError('Missing product column.')
df = df.rename(columns={pcol: 'product'})
# amount
df = _parse_amount_col(df)
# parse dates
if not np.issubdtype(df['date'].dtype, np.datetime64):
df['date'] = pd.to_datetime(df['date'], errors='coerce', utc=False)
df = df.dropna(subset=['date'])
df['date'] = df['date'].dt.tz_localize(None) if hasattr(df['date'].dt, 'tz_localize') else df['date']
# add calendar helpers
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.to_period('M').dt.to_timestamp()
df['quarter'] = df['date'].dt.to_period('Q').astype(str)
return df
def _build_summary(df: pd.DataFrame) -> dict:
total_orders = int(len(df))
total_revenue = float(df['amount'].sum())
avg_order = float(df['amount'].mean()) if total_orders else 0.0
start = df['date'].min()
end = df['date'].max()
regions_count = int(df['region'].nunique())
products_count = int(df['product'].nunique())
by_region = df.groupby('region', as_index=False)['amount'].sum().sort_values('amount', ascending=False)
top_region = by_region.iloc[0]['region'] if len(by_region) else None
by_month = df.groupby('month', as_index=False)['amount'].sum().sort_values('month')
mom_change = None
if len(by_month) >= 2:
last, prev = by_month.iloc[-1]['amount'], by_month.iloc[-2]['amount']
if prev != 0:
chg = (last - prev) / prev * 100.0
arrow = '▲' if chg >= 0 else '▼'
mom_change = f"{arrow} {chg:.1f}% vs prev month"
else:
mom_change = "—"
summary = {
'total_orders': total_orders,
'total_revenue': total_revenue,
'avg_order_value': avg_order,
'total_revenue_fmt': _fmt_currency(total_revenue),
'avg_order_value_fmt': _fmt_currency(avg_order),
'date_range': f"{start.date()} → {end.date()}" if start is not pd.NaT and end is not pd.NaT else '—',
'regions_count': regions_count,
'products_count': products_count,
'top_region': top_region,
'mom_change': mom_change,
}
return summary
def _fig_to_data_uri(fig) -> str:
buf = io.BytesIO()
fig.savefig(buf, format='png', bbox_inches='tight', dpi=140, facecolor='#0f1736')
plt.close(fig)
buf.seek(0)
b64 = base64.b64encode(buf.read()).decode('ascii')
return f"data:image/png;base64,{b64}"
def _style_axes(ax):
ax.grid(True, color='#27406b', alpha=0.5, linestyle='--', linewidth=0.6)
ax.set_facecolor('#0f1736')
for spine in ax.spines.values():
spine.set_color('#2a3a6b')
ax.tick_params(colors='#e6eeff')
def _ingest_csv(csv_text: str) -> str:
"""Helper to load CSV text into the agent state.
Args:
csv_text: CSV content as a UTF-8 string.
"""
try:
df = pd.read_csv(io.StringIO(csv_text))
df = _standardize_columns(df)
_STATE['df'] = df
summary = _build_summary(df)
_STATE['summary'] = summary
return json.dumps({'success': True, 'summary': summary})
except Exception as e:
_STATE['df'] = None
_STATE['summary'] = None
return json.dumps({'success': False, 'error': str(e)})
# Public tools (auto-exposed)
def get_summary_cards() -> str:
"""Return key sales summary metrics as JSON.
Args:
None
"""
df = _ensure_df()
summary = _build_summary(df)
_STATE['summary'] = summary
return json.dumps(summary)
def chart_monthly_trend() -> str:
"""Render a monthly revenue trend chart. Returns a PNG data URI string.
Args:
None
"""
df = _ensure_df()
monthly = df.groupby('month', as_index=False)['amount'].sum().sort_values('month')
fig, ax = plt.subplots(figsize=(6.6, 3.4))
_style_axes(ax)
ax.plot(monthly['month'], monthly['amount'], color='#5aa7ff', linewidth=2.0, marker='o', markersize=3)
ax.set_title('Monthly Revenue', color='#e6eeff')
ax.set_xlabel('Month', color='#e6eeff')
ax.set_ylabel('Revenue', color='#e6eeff')
fig.autofmt_xdate()
return _fig_to_data_uri(fig)
def chart_top_products(n: int = 5) -> str:
"""Render a bar chart of top-N products by revenue. Returns a PNG data URI.
Args:
n: Number of products to include.
"""
df = _ensure_df()
top = (
df.groupby('product', as_index=False)['amount']
.sum()
.sort_values('amount', ascending=False)
.head(int(n))
.sort_values('amount', ascending=True)
)
fig, ax = plt.subplots(figsize=(6.6, max(3.0, 0.38*len(top)+2)))
_style_axes(ax)
ax.barh(top['product'], top['amount'], color='#7ed9a8')
ax.set_title(f'Top {len(top)} Products by Revenue', color='#e6eeff')
ax.set_xlabel('Revenue', color='#e6eeff')
return _fig_to_data_uri(fig)
def chart_revenue_by_region() -> str:
"""Render a bar chart of revenue by region. Returns a PNG data URI.
Args:
None
"""
df = _ensure_df()
reg = df.groupby('region', as_index=False)['amount'].sum().sort_values('amount', ascending=True)
fig, ax = plt.subplots(figsize=(6.6, max(3.0, 0.38*len(reg)+2)))
_style_axes(ax)
ax.barh(reg['region'], reg['amount'], color='#5aa7ff')
ax.set_title('Revenue by Region', color='#e6eeff')
ax.set_xlabel('Revenue', color='#e6eeff')
return _fig_to_data_uri(fig)
def pivot(group_by: str, period: str = 'M', top_n: int = 10) -> str:
"""Aggregate revenue by time period and a category. Returns compact JSON.
Args:
group_by: One of 'region' or 'product'.
period: Time bucket. One of 'D','M','Q','Y'. Default 'M'.
top_n: Limit number of groups by total revenue.
"""
df = _ensure_df()
group_by = (group_by or '').strip().lower()
if group_by not in ('region', 'product'):
raise ValueError("group_by must be 'region' or 'product'")
period = (period or 'M').upper()
if period not in ('D','M','Q','Y'):
raise ValueError("period must be one of D,M,Q,Y")
# Determine time key
if period == 'D':
df['_time'] = df['date'].dt.to_period('D').dt.to_timestamp()
elif period == 'M':
df['_time'] = df['date'].dt.to_period('M').dt.to_timestamp()
elif period == 'Q':
df['_time'] = df['date'].dt.to_period('Q').dt.to_timestamp()
else:
df['_time'] = pd.to_datetime(df['date'].dt.year.astype(str) + '-01-01')
# Top-N groups by total
totals = df.groupby(group_by, as_index=False)['amount'].sum().sort_values('amount', ascending=False)
keep = set(totals.head(int(top_n))[group_by].tolist())
df2 = df[df[group_by].isin(keep)].copy()
agg = df2.groupby([group_by, '_time'], as_index=False)['amount'].sum().sort_values(['_time', group_by])
out = {
'group_by': group_by,
'period': period,
'rows': [
{
group_by: str(r[group_by]),
'time': r['_time'].strftime('%Y-%m-%d'),
'revenue': float(r['amount'])
}
for _, r in agg.iterrows()
]
}
# Cleanup temp column
if '_time' in df:
del df['_time']
s = json.dumps(out)
return _truncate(s)
Name your agent
Based on . You'll get your own private copy to try and customize.