mirror of
https://github.com/csunny/DB-GPT.git
synced 2026-07-17 01:58:47 +00:00
feat: add csv analysis skill
This commit is contained in:
106
skills/csv-data-analysis/SKILL.md
Normal file
106
skills/csv-data-analysis/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: csv-data-analysis
|
||||
description: This skill should be used when users need to analyze CSV files, understand data patterns, generate statistical summaries, or create data visualizations. Trigger keywords include "分析CSV", "数据分析", "CSV分析", "数据统计", "生成图表", "数据可视化".
|
||||
---
|
||||
|
||||
# 智能 CSV 数据深度分析工具
|
||||
|
||||
CSV数据分析工具是一个基于 AI 与前端可视化技术(ECharts + Tailwind CSS)的深度自动化数据探索工具。它能够快速提取统计特征、分类信息、相关性以及时序趋势,并由大模型注入深度业务洞察,生成高度美观和可交互的网页分析报告。
|
||||
|
||||
## 核心工作流(LLM 必读)
|
||||
|
||||
作为 AI 助手,在用户上传 CSV 并要求分析时,你需要严格按照以下两步执行:
|
||||
|
||||
### 第一步:提取数据特征 (执行脚本)
|
||||
|
||||
使用 `execute_skill_script_file` 工具运行 `csv_analyzer.py`,将 CSV 文件路径传入。
|
||||
|
||||
**工具调用参数示例:**
|
||||
```json
|
||||
{
|
||||
"skill_name": "csv-data-analysis",
|
||||
"script_file_name": "csv_analyzer.py",
|
||||
"args": {"input_file": "/path/to/data.csv"}
|
||||
}
|
||||
```
|
||||
|
||||
**脚本返回说明:**
|
||||
脚本会返回一大段 `text` 内容,其中包含两个部分:
|
||||
1. **【统计摘要】**:供你阅读并理解数据集的基本情况、分布、相关性和分类构成。
|
||||
2. **【CHART_DATA_JSON】**:位于 `###CHART_DATA_JSON_START###` 和 `###CHART_DATA_JSON_END###` 之间的纯 JSON 字符串。这是用于渲染交互式图表的原生数据。
|
||||
|
||||
### 第二步:生成洞察与展示报告 (注入模板)
|
||||
|
||||
阅读第一步获得的"统计摘要",思考数据背后的业务意义或规律。然后使用 `html_interpreter` 工具,加载模板并注入数据。
|
||||
|
||||
**关键规则(必须遵守):**
|
||||
|
||||
1. **必须设置 `template_path`** 为 `csv-data-analysis/templates/report_template.html`。模板中已内置完整的 ECharts 渲染 JavaScript 代码和所有章节标题、页脚文本,你只需要通过 `data` 参数填充 9 个内容占位符即可。**绝对不要自己编写或修改任何 JavaScript 图表渲染代码。**
|
||||
|
||||
2. **`CHART_DATA_JSON`** 必须**完整且原封不动**地复制自脚本输出中 `###CHART_DATA_JSON_START###` 和 `###CHART_DATA_JSON_END###` 之间的纯 JSON 字符串。不要自行编造,不需要做任何转义处理。
|
||||
|
||||
3. **`*_INSIGHTS`、`EXEC_SUMMARY` 和 `CONCLUSIONS`** 必须使用 HTML 格式(如 `<p>`, `<ul>`, `<li>`, `<strong>`, `<ol>`)来确保排版美观。这些内容由你基于统计摘要撰写深度业务洞察。
|
||||
|
||||
4. **输出语言必须与用户输入语言一致。**
|
||||
|
||||
5. **只传 9 个占位符,不要多也不要少。** 模板已将所有章节标题(Distribution Analysis、Correlation Analysis 等)、洞察框标题(Insights)和页脚文本硬编码在 HTML 中,你无需传递这些。
|
||||
|
||||
**`html_interpreter` 调用示例:**
|
||||
```json
|
||||
{
|
||||
"template_path": "csv-data-analysis/templates/report_template.html",
|
||||
"data": {
|
||||
"REPORT_TITLE": "销售数据集深度分析报告",
|
||||
"REPORT_SUBTITLE": "多维度数据特征与业务洞见挖掘",
|
||||
"EXEC_SUMMARY": "<p>本数据集共包含 1000 行 5 列,数据完整性良好。核心洞察如下:</p><ul><li><strong>受众分布:</strong>主要集中在 25-35 岁群体...</li></ul>",
|
||||
"DISTRIBUTION_INSIGHTS": "<p>从数值分布图可以看出,指标 A 呈现出明显的右偏态分布,建议...</p>",
|
||||
"CORRELATION_INSIGHTS": "<p>变量间的热力图揭示了强烈的正相关关系,特别是...,这意味着...</p>",
|
||||
"CATEGORICAL_INSIGHTS": "<p>分类占比显示,'城市'字段中北京与上海占据了 50% 以上的份额。</p>",
|
||||
"TIME_SERIES_INSIGHTS": "<p>从时序趋势中可以看出,数据在年末存在显著的季节性拉升现象。</p>",
|
||||
"CONCLUSIONS": "<p>综合以上多维度分析,数据呈现出明确的结构性特征与规律。</p><h3>建议</h3><ul><li>建议定期检查缺失值比例...</li><li>重点关注高增长细分市场...</li></ul>",
|
||||
"CHART_DATA_JSON": "此处粘贴脚本输出中 ###CHART_DATA_JSON_START### 到 ###CHART_DATA_JSON_END### 之间的完整 JSON 字符串"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **严禁事项:**
|
||||
> - 禁止在 `data` 中添加任何 JavaScript 代码
|
||||
> - 禁止省略 `template_path` 参数(不设置 template_path 会导致图表无法渲染!)
|
||||
> - 禁止对 `CHART_DATA_JSON` 做任何修改、截断或二次编码
|
||||
> - 禁止返回静态 PNG 图片,本工具已全面升级为 ECharts 动态前端渲染
|
||||
> - 禁止传递不存在的占位符(模板只有以下 9 个占位符,传递其他名称会被忽略)
|
||||
|
||||
## 占位符清单(共 9 个)
|
||||
|
||||
模板中的全部占位符如下(通过 `data` 参数传递):
|
||||
|
||||
| 占位符 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `REPORT_TITLE` | 文本 | 是 | 报告标题,如"销售数据集深度分析报告" |
|
||||
| `REPORT_SUBTITLE` | 文本 | 是 | 报告副标题,如"多维度数据特征与业务洞见挖掘" |
|
||||
| `EXEC_SUMMARY` | HTML | 是 | 执行摘要(你撰写的深度总结),使用 `<p>`, `<ul>`, `<li>` 等标签 |
|
||||
| `DISTRIBUTION_INSIGHTS` | HTML | 是 | 数值分布洞察正文 |
|
||||
| `CORRELATION_INSIGHTS` | HTML | 是 | 相关性分析洞察正文 |
|
||||
| `CATEGORICAL_INSIGHTS` | HTML | 是 | 分类变量洞察正文 |
|
||||
| `TIME_SERIES_INSIGHTS` | HTML | 是 | 时间序列洞察正文 |
|
||||
| `CONCLUSIONS` | HTML | 是 | 结论与建议正文(包含建议内容,直接写在此字段中) |
|
||||
| `CHART_DATA_JSON` | JSON字符串 | 是 | 脚本输出的原始 JSON(驱动图表渲染),必须从脚本输出原封不动复制 |
|
||||
|
||||
> **注意:** 模板中的所有章节标题(如 "Distribution Analysis"、"Correlation Analysis"、"Conclusions & Recommendations" 等)、洞察框标题("Insights")和页脚文本已硬编码在 HTML 中,无需通过占位符传递。
|
||||
|
||||
## 为什么选择本工具?
|
||||
|
||||
1. **极速与轻量**:告别缓慢的 Python 绘图和大量 PNG 生成,只传输核心 JSON 数据。
|
||||
2. **现代交互式排版**:全面接入 Tailwind CSS 响应式布局和 Apache ECharts 丝滑动画交互。
|
||||
3. **深度业务洞见**:通过将机器的数据提炼和 LLM 的逻辑推理分离,能产出极具含金量的数据分析报告。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
csv-data-analysis/
|
||||
├── SKILL.md # 你当前正在阅读的技能指南
|
||||
├── scripts/
|
||||
│ └── csv_analyzer.py # Python 分析引擎(轻量级、无图形依赖)
|
||||
└── templates/
|
||||
└── report_template.html # 响应式 ECharts 报表模板(内含完整渲染逻辑与硬编码标题)
|
||||
```
|
||||
15
skills/csv-data-analysis/references/reference.md
Normal file
15
skills/csv-data-analysis/references/reference.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Reference Documentation
|
||||
|
||||
This file contains detailed reference material for the skill.
|
||||
|
||||
## API Reference
|
||||
|
||||
[TODO: Add detailed API documentation here]
|
||||
|
||||
## Schema Definitions
|
||||
|
||||
[TODO: Add schema definitions if applicable]
|
||||
|
||||
## Detailed Workflows
|
||||
|
||||
[TODO: Add detailed workflow descriptions]
|
||||
328
skills/csv-data-analysis/scripts/csv_analyzer.py
Normal file
328
skills/csv-data-analysis/scripts/csv_analyzer.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import os
|
||||
import json
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
def log(*args, **kwargs):
|
||||
"""将日志输出到 stderr,避免污染 stdout 的 JSON 输出"""
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
def analyze_csv(file_path):
|
||||
"""
|
||||
分析CSV文件,提取用于 ECharts 渲染的数据结构和用于 LLM 分析的统计摘要。
|
||||
输出包含: overview, distributions, correlations, categories, time_series,
|
||||
scatter (散点图), stats_table (统计表格)
|
||||
"""
|
||||
try:
|
||||
log(f"正在读取文件: {file_path}")
|
||||
df = pd.read_csv(file_path)
|
||||
|
||||
# ==========================================
|
||||
# 1. 基础概览数据
|
||||
# ==========================================
|
||||
total_cells = int(df.shape[0] * df.shape[1])
|
||||
missing_cells = int(df.isnull().sum().sum())
|
||||
missing_pct = (
|
||||
round((missing_cells / total_cells) * 100, 2) if total_cells > 0 else 0
|
||||
)
|
||||
|
||||
overview = {
|
||||
"rows": int(df.shape[0]),
|
||||
"cols": int(df.shape[1]),
|
||||
"missing_cells": missing_cells,
|
||||
"missing_pct": missing_pct,
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# 2. 数值列分析 (直方图分布 & 相关性)
|
||||
# ==========================================
|
||||
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
|
||||
distributions = {}
|
||||
correlations = {"cols": numeric_cols, "data": []}
|
||||
numeric_summary = {}
|
||||
|
||||
if numeric_cols:
|
||||
# 取最多前 8 个数值列画分布图
|
||||
for col in numeric_cols[:8]:
|
||||
s = df[col].dropna()
|
||||
if len(s) > 0:
|
||||
# 使用 numpy 计算直方图 (10个 bin)
|
||||
hist, bin_edges = np.histogram(s, bins=10)
|
||||
bins = [
|
||||
f"{bin_edges[i]:.1f}~{bin_edges[i + 1]:.1f}"
|
||||
for i in range(len(hist))
|
||||
]
|
||||
distributions[col] = {
|
||||
"bins": bins,
|
||||
"counts": [int(x) for x in hist],
|
||||
}
|
||||
numeric_summary[col] = {
|
||||
"min": float(s.min()),
|
||||
"max": float(s.max()),
|
||||
"mean": float(s.mean()),
|
||||
"median": float(s.median()),
|
||||
"std": float(s.std()),
|
||||
"q25": float(s.quantile(0.25)),
|
||||
"q75": float(s.quantile(0.75)),
|
||||
}
|
||||
|
||||
# 相关性矩阵 (取全部数值列)
|
||||
if len(numeric_cols) > 1:
|
||||
corr_df = df[numeric_cols].corr(method="pearson").fillna(0).round(2) # type: ignore[call-overload]
|
||||
for i, col1 in enumerate(numeric_cols):
|
||||
for j, col2 in enumerate(numeric_cols):
|
||||
correlations["data"].append([i, j, float(corr_df.iloc[i, j])])
|
||||
|
||||
# ==========================================
|
||||
# 3. 分类列分析 (饼图/柱状图)
|
||||
# ==========================================
|
||||
categorical_cols = df.select_dtypes(
|
||||
include=["object", "category"]
|
||||
).columns.tolist()
|
||||
categories = {}
|
||||
cat_summary = {}
|
||||
|
||||
if categorical_cols:
|
||||
# 取最多前 6 个分类列
|
||||
for col in categorical_cols[:6]:
|
||||
if df[col].nunique() <= 50:
|
||||
val_counts = df[col].value_counts().head(10)
|
||||
if len(val_counts) > 0:
|
||||
categories[col] = {
|
||||
"labels": [str(x) for x in val_counts.index.tolist()],
|
||||
"values": [int(x) for x in val_counts.values],
|
||||
}
|
||||
top1 = val_counts.index[0]
|
||||
top1_count = val_counts.values[0]
|
||||
cat_summary[col] = (
|
||||
f"唯一值数量: {df[col].nunique()},最常见: {top1} (出现 {top1_count} 次)"
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# 4. 时间序列分析
|
||||
# ==========================================
|
||||
time_series = {"name": "", "dates": [], "values": []}
|
||||
|
||||
if numeric_cols:
|
||||
date_col = None
|
||||
for col in df.columns:
|
||||
if df[col].dtype == "object":
|
||||
try:
|
||||
pd.to_datetime(df[col].dropna().head(100))
|
||||
date_col = col
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if date_col:
|
||||
num_col = numeric_cols[0]
|
||||
df_ts = df.copy()
|
||||
df_ts[date_col] = pd.to_datetime(df_ts[date_col], errors="coerce")
|
||||
df_ts = df_ts.dropna(subset=[date_col, num_col])
|
||||
|
||||
if not df_ts.empty:
|
||||
df_ts = df_ts.set_index(date_col)
|
||||
try:
|
||||
monthly = df_ts[num_col].resample("M").mean().dropna()
|
||||
if len(monthly) < 3:
|
||||
monthly = df_ts[num_col].resample("D").mean().dropna()
|
||||
monthly = monthly.tail(100)
|
||||
|
||||
time_series["name"] = num_col
|
||||
time_series["dates"] = [
|
||||
x.strftime("%Y-%m-%d") for x in monthly.index
|
||||
]
|
||||
time_series["values"] = [
|
||||
round(float(x), 2) for x in monthly.values
|
||||
]
|
||||
except Exception as e:
|
||||
log(f"时间序列处理失败: {e}")
|
||||
|
||||
# ==========================================
|
||||
# 5. 散点图数据 (前两个数值列)
|
||||
# ==========================================
|
||||
scatter = {}
|
||||
if len(numeric_cols) >= 2:
|
||||
col_x, col_y = numeric_cols[0], numeric_cols[1]
|
||||
df_scatter = df[[col_x, col_y]].dropna()
|
||||
# 限制最多 500 个点,避免数据过大
|
||||
if len(df_scatter) > 500:
|
||||
df_scatter = df_scatter.sample(500, random_state=42)
|
||||
scatter = {
|
||||
"x_name": col_x,
|
||||
"y_name": col_y,
|
||||
"x": [round(float(v), 4) for v in df_scatter[col_x].tolist()],
|
||||
"y": [round(float(v), 4) for v in df_scatter[col_y].tolist()],
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# 6. 统计汇总表格
|
||||
# ==========================================
|
||||
stats_table = {"headers": [], "rows": []}
|
||||
if numeric_summary:
|
||||
stats_table["headers"] = [
|
||||
"变量",
|
||||
"最小值",
|
||||
"Q25",
|
||||
"中位数",
|
||||
"均值",
|
||||
"Q75",
|
||||
"最大值",
|
||||
"标准差",
|
||||
]
|
||||
for col, s in numeric_summary.items():
|
||||
stats_table["rows"].append(
|
||||
[
|
||||
col,
|
||||
round(s["min"], 2),
|
||||
round(s["q25"], 2),
|
||||
round(s["median"], 2),
|
||||
round(s["mean"], 2),
|
||||
round(s["q75"], 2),
|
||||
round(s["max"], 2),
|
||||
round(s["std"], 2),
|
||||
]
|
||||
)
|
||||
|
||||
# ==========================================
|
||||
# 构建给 ECharts 渲染的完整 JSON 数据结构
|
||||
# ==========================================
|
||||
chart_data = {
|
||||
"overview": overview,
|
||||
"numeric_cols": numeric_cols,
|
||||
"distributions": distributions,
|
||||
"correlations": correlations,
|
||||
"categories": categories,
|
||||
"time_series": time_series,
|
||||
"scatter": scatter,
|
||||
"stats_table": stats_table,
|
||||
}
|
||||
|
||||
chart_data_json_str = json.dumps(chart_data, ensure_ascii=False)
|
||||
|
||||
# ==========================================
|
||||
# 构建给 LLM 深度分析阅读的文本摘要
|
||||
# ==========================================
|
||||
summary_lines = [
|
||||
"==================================================",
|
||||
"【数据概览】",
|
||||
f"- 数据集尺寸: {overview['rows']} 行 × {overview['cols']} 列",
|
||||
f"- 缺失值情况: 共有 {overview['missing_cells']} 个单元格缺失,整体数据完整率 {100 - overview['missing_pct']}%",
|
||||
f"- 数值型列 ({len(numeric_cols)}): {', '.join(numeric_cols[:10])}",
|
||||
f"- 分类型列 ({len(categorical_cols)}): {', '.join(categorical_cols[:10])}",
|
||||
"",
|
||||
"【数值型特征统计 (Top 8)】",
|
||||
]
|
||||
for col, s in numeric_summary.items():
|
||||
summary_lines.append(
|
||||
f"- {col}: min={s['min']:.2f}, Q25={s['q25']:.2f}, median={s['median']:.2f}, "
|
||||
f"mean={s['mean']:.2f}, Q75={s['q75']:.2f}, max={s['max']:.2f}, std={s['std']:.2f}"
|
||||
)
|
||||
|
||||
summary_lines.append("")
|
||||
summary_lines.append("【分类型特征摘要 (Top 6)】")
|
||||
for col, stats in cat_summary.items():
|
||||
summary_lines.append(f"- {col}: {stats}")
|
||||
|
||||
summary_lines.append("")
|
||||
summary_lines.append("【核心相关性】")
|
||||
if correlations["data"]:
|
||||
strong_corrs = []
|
||||
for item in correlations["data"]:
|
||||
i, j, val = item
|
||||
if i < j and abs(val) >= 0.5:
|
||||
strong_corrs.append(
|
||||
f"{numeric_cols[i]} 与 {numeric_cols[j]} (相关系数: {val})"
|
||||
)
|
||||
if strong_corrs:
|
||||
summary_lines.extend([f"- {c}" for c in strong_corrs])
|
||||
else:
|
||||
summary_lines.append("- 没有发现强相关的数值变量组合(|r| >= 0.5)。")
|
||||
|
||||
if scatter:
|
||||
summary_lines.append("")
|
||||
summary_lines.append(
|
||||
f"【散点图】已生成 {scatter['x_name']} vs {scatter['y_name']} 的散点图数据"
|
||||
)
|
||||
|
||||
summary_lines.append("==================================================")
|
||||
summary_lines.append(
|
||||
"请作为数据分析专家,基于以上【统计摘要】为用户撰写深度的数据分析见解(Insights)。"
|
||||
)
|
||||
summary_lines.append(
|
||||
"并且,在使用 html_interpreter 时,请将下方 JSON_START 和 JSON_END 标记之间的纯 JSON 字符串完整传递给变量 CHART_DATA_JSON。"
|
||||
)
|
||||
summary_lines.append("###CHART_DATA_JSON_START###")
|
||||
summary_lines.append(chart_data_json_str)
|
||||
summary_lines.append("###CHART_DATA_JSON_END###")
|
||||
|
||||
final_text = "\n".join(summary_lines)
|
||||
|
||||
# 输出标准 chunks
|
||||
print(
|
||||
json.dumps(
|
||||
{"chunks": [{"output_type": "text", "content": final_text}]},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
err_msg = f"分析过程中出现错误: {str(e)}\n{traceback.format_exc()}"
|
||||
print(
|
||||
json.dumps(
|
||||
{"chunks": [{"output_type": "text", "content": err_msg}]},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
result = {
|
||||
"chunks": [
|
||||
{
|
||||
"output_type": "text",
|
||||
"content": '使用方法: python csv_analyzer.py \'{"input_file": "data.csv"}\'',
|
||||
}
|
||||
]
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
args = json.loads(sys.argv[1])
|
||||
csv_file = (
|
||||
args.get("input_file") or args.get("file_path") or args.get("csv_file", "")
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
csv_file = sys.argv[1]
|
||||
|
||||
if not csv_file or not os.path.exists(csv_file):
|
||||
result = {
|
||||
"chunks": [{"output_type": "text", "content": f"文件不存在: {csv_file}"}]
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
sys.exit(1)
|
||||
|
||||
if not csv_file.lower().endswith(".csv"):
|
||||
result = {
|
||||
"chunks": [
|
||||
{"output_type": "text", "content": f"文件不是CSV格式: {csv_file}"}
|
||||
]
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
sys.exit(1)
|
||||
|
||||
analyze_csv(csv_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
skills/csv-data-analysis/scripts/example.py
Executable file
24
skills/csv-data-analysis/scripts/example.py
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Example script for the skill.
|
||||
|
||||
This script can be called by Claude to perform deterministic operations.
|
||||
Replace this with your actual skill logic.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python3 example.py <input>")
|
||||
sys.exit(1)
|
||||
|
||||
input_data = sys.argv[1]
|
||||
# TODO: Add your skill logic here
|
||||
result = {"status": "success", "input": input_data}
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
534
skills/csv-data-analysis/templates/report_template.html
Normal file
534
skills/csv-data-analysis/templates/report_template.html
Normal file
@@ -0,0 +1,534 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{REPORT_TITLE}}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--primary: #4f46e5;
|
||||
--primary-light: #818cf8;
|
||||
--primary-bg: #eef2ff;
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-green: #10b981;
|
||||
--accent-amber: #f59e0b;
|
||||
--accent-rose: #f43f5e;
|
||||
--accent-purple: #8b5cf6;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--bg-page: #f8fafc;
|
||||
--card-bg: #ffffff;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', system-ui, sans-serif;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-primary);
|
||||
margin: 0; padding: 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.prose p { margin-bottom: 0.75em; line-height: 1.8; }
|
||||
.prose ul { list-style: disc; padding-left: 1.5em; margin-bottom: 0.75em; }
|
||||
.prose ol { list-style: decimal; padding-left: 1.5em; margin-bottom: 0.75em; }
|
||||
.prose li { margin-bottom: 0.4em; }
|
||||
.prose strong { color: var(--primary); font-weight: 600; }
|
||||
.prose h3 { font-size: 1.1em; font-weight: 600; color: var(--text-primary); margin: 1em 0 0.5em; }
|
||||
|
||||
.section-badge {
|
||||
display: inline-flex; align-items: center; gap: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||||
color: #fff; padding: 0.35rem 1rem; border-radius: 2rem;
|
||||
font-weight: 600; font-size: 0.95rem; letter-spacing: 0.02em;
|
||||
box-shadow: 0 2px 8px rgba(79,70,229,0.25);
|
||||
}
|
||||
.section-badge svg { width: 18px; height: 18px; }
|
||||
|
||||
.metric-card {
|
||||
background: #fff; border-radius: 12px; padding: 1.25rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.metric-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.08); }
|
||||
.metric-card .label { font-size: 0.85rem; color: var(--text-secondary); font-weight: 500; margin-bottom: 0.25rem; }
|
||||
.metric-card .value { font-size: 1.5rem; font-weight: 700; color: var(--text-primary); }
|
||||
|
||||
.insight-box {
|
||||
background: linear-gradient(135deg, #eff6ff 0%, #f0f4ff 100%);
|
||||
border-left: 4px solid var(--accent-blue);
|
||||
padding: 1.25rem 1.5rem; border-radius: 0 8px 8px 0;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.insight-box .insight-title {
|
||||
font-weight: 700; color: var(--accent-blue); margin-bottom: 0.5rem;
|
||||
display: flex; align-items: center; gap: 0.4rem; font-size: 1rem;
|
||||
}
|
||||
|
||||
.data-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin: 1rem 0; }
|
||||
.data-table th {
|
||||
background: #f1f5f9; color: var(--text-primary); font-weight: 600;
|
||||
padding: 0.75rem 1rem; text-align: left; border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
.data-table td { padding: 0.65rem 1rem; border-bottom: 1px solid #f1f5f9; }
|
||||
.data-table tr:hover td { background: #f8fafc; }
|
||||
.data-table .num { font-weight: 600; color: var(--primary); }
|
||||
|
||||
.chart-card {
|
||||
background: #fff; border-radius: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 16px rgba(0,0,0,0.04);
|
||||
padding: 2rem; margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chart-card-header {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
margin-bottom: 1.5rem; padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
.chart-card-header h2 {
|
||||
font-size: 1.35rem; font-weight: 700; color: var(--text-primary); margin: 0;
|
||||
}
|
||||
.chart-card-header .icon {
|
||||
width: 36px; height: 36px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem; flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.chart-card { break-inside: avoid; }
|
||||
body { background: #fff; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ============ HEADER ============ -->
|
||||
<header style="background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #a855f7 100%); position: relative; overflow: hidden;">
|
||||
<div style="position: absolute; top: -50%; left: -10%; width: 60%; height: 200%; background: radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 70%); pointer-events: none;"></div>
|
||||
<div style="position: absolute; bottom: -30%; right: -5%; width: 40%; height: 160%; background: radial-gradient(circle, rgba(255,255,255,0.05) 0%, transparent 70%); pointer-events: none;"></div>
|
||||
<div class="max-w-6xl mx-auto px-6 py-16 text-center relative" style="z-index: 1;">
|
||||
<h1 style="font-size: 2.5rem; font-weight: 800; color: #fff; margin: 0 0 0.75rem; letter-spacing: 0.02em; text-shadow: 0 2px 12px rgba(0,0,0,0.15);">{{REPORT_TITLE}}</h1>
|
||||
<p style="font-size: 1.15rem; color: rgba(255,255,255,0.85); margin: 0; font-weight: 400;">{{REPORT_SUBTITLE}}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto px-4 py-10" style="display: flex; flex-direction: column; gap: 2rem;">
|
||||
|
||||
<!-- ============ SECTION 1: EXEC SUMMARY ============ -->
|
||||
<div class="chart-card">
|
||||
<div style="margin-bottom: 1.25rem;">
|
||||
<span class="section-badge">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
|
||||
Executive Summary
|
||||
</span>
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{EXEC_SUMMARY}}</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6" id="overview-metrics"></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 2: DISTRIBUTIONS ============ -->
|
||||
<div class="chart-card" id="distribution-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #dbeafe; color: #2563eb;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
||||
</div>
|
||||
<h2>Distribution Analysis</h2>
|
||||
</div>
|
||||
<div id="distribution-charts" class="grid grid-cols-1 md:grid-cols-2 gap-6"></div>
|
||||
<div class="insight-box">
|
||||
<div class="insight-title">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Insights
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{DISTRIBUTION_INSIGHTS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 3: CORRELATION ============ -->
|
||||
<div class="chart-card" id="correlation-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #fce7f3; color: #db2777;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
</div>
|
||||
<h2>Correlation Analysis</h2>
|
||||
</div>
|
||||
<div id="correlation-chart" style="height: 500px; width: 100%;"></div>
|
||||
<div class="insight-box">
|
||||
<div class="insight-title">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||
Insights
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{CORRELATION_INSIGHTS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 4: CATEGORICAL ============ -->
|
||||
<div class="chart-card" id="categorical-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #d1fae5; color: #059669;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z"/></svg>
|
||||
</div>
|
||||
<h2>Categorical Analysis</h2>
|
||||
</div>
|
||||
<div id="categorical-charts" class="grid grid-cols-1 md:grid-cols-2 gap-6"></div>
|
||||
<div id="topn-chart" style="height: 400px; width: 100%; margin-top: 1.5rem;"></div>
|
||||
<div class="insight-box">
|
||||
<div class="insight-title">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
||||
Insights
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{CATEGORICAL_INSIGHTS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 5: TIME SERIES ============ -->
|
||||
<div class="chart-card" id="time-series-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #ede9fe; color: #7c3aed;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
||||
</div>
|
||||
<h2>Time Series Analysis</h2>
|
||||
</div>
|
||||
<div id="time-series-chart" style="height: 400px; width: 100%;"></div>
|
||||
<div class="insight-box">
|
||||
<div class="insight-title">
|
||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Insights
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{TIME_SERIES_INSIGHTS}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 6: RADAR / SCATTER ============ -->
|
||||
<div class="chart-card" id="advanced-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #fef3c7; color: #d97706;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
|
||||
</div>
|
||||
<h2>Advanced Analysis</h2>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div id="radar-chart" style="height: 400px; width: 100%;"></div>
|
||||
<div id="scatter-chart" style="height: 400px; width: 100%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 7: DATA TABLE ============ -->
|
||||
<div class="chart-card" id="table-section">
|
||||
<div class="chart-card-header">
|
||||
<div class="icon" style="background: #e0e7ff; color: #4338ca;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M3 14h18m-9-4v8m-7 0h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
</div>
|
||||
<h2>Statistical Summary</h2>
|
||||
</div>
|
||||
<div id="stats-table-container" style="overflow-x: auto;"></div>
|
||||
</div>
|
||||
|
||||
<!-- ============ SECTION 8: CONCLUSIONS ============ -->
|
||||
<div class="chart-card">
|
||||
<div style="margin-bottom: 1.25rem;">
|
||||
<span class="section-badge" style="background: linear-gradient(135deg, #059669 0%, #10b981 100%); box-shadow: 0 2px 8px rgba(5,150,105,0.25);">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Conclusions & Recommendations
|
||||
</span>
|
||||
</div>
|
||||
<div class="prose max-w-none" style="color: var(--text-secondary);">{{CONCLUSIONS}}</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- ============ FOOTER ============ -->
|
||||
<footer style="background: #1e293b; color: #94a3b8; text-align: center; padding: 2.5rem 1rem;">
|
||||
<p style="margin: 0 0 0.5rem; font-size: 0.95rem;">Powered by AI Data Analysis Engine</p>
|
||||
<p style="margin: 0; font-size: 0.8rem; opacity: 0.7;">Auto-generated interactive report</p>
|
||||
</footer>
|
||||
|
||||
<!-- ============ CHART DATA (safe JSON injection) ============ -->
|
||||
<script type="application/json" id="echart-binddata">
|
||||
{{CHART_DATA_JSON}}
|
||||
</script>
|
||||
|
||||
<!-- ============ RENDERING LOGIC ============ -->
|
||||
<script>
|
||||
(function() {
|
||||
var C = {};
|
||||
try {
|
||||
var el = document.getElementById("echart-binddata");
|
||||
if (el) {
|
||||
var raw = (el.textContent || "").trim();
|
||||
if (raw.charAt(0) === "{") C = JSON.parse(raw);
|
||||
}
|
||||
} catch(e) { console.error("Parse error:", e); }
|
||||
|
||||
var I = [];
|
||||
var COLORS = ["#6366f1","#3b82f6","#06b6d4","#10b981","#f59e0b","#f43f5e","#8b5cf6","#ec4899","#14b8a6","#84cc16"];
|
||||
|
||||
function mkDiv(parent, h) {
|
||||
var d = document.createElement("div");
|
||||
d.style.cssText = "height:" + h + "px;width:100%;border:1px solid #f1f5f9;border-radius:8px;padding:4px;background:#fff;";
|
||||
parent.appendChild(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
|
||||
// ======== 1. OVERVIEW METRIC CARDS ========
|
||||
if (C.overview) {
|
||||
var ov = C.overview;
|
||||
var mc = document.getElementById("overview-metrics");
|
||||
if (mc) {
|
||||
var items = [
|
||||
{l:"Total Rows", v: ov.rows ? ov.rows.toLocaleString() : "N/A", icon:"#6366f1"},
|
||||
{l:"Total Cols", v: ov.cols || "N/A", icon:"#3b82f6"},
|
||||
{l:"Missing Cells", v: ov.missing_cells != null ? ov.missing_cells.toLocaleString() : "0", icon:"#f59e0b"},
|
||||
{l:"Completeness", v: ov.missing_pct != null ? (100 - ov.missing_pct).toFixed(1) + "%" : "100%", icon:"#10b981"}
|
||||
];
|
||||
items.forEach(function(m) {
|
||||
var card = document.createElement("div");
|
||||
card.className = "metric-card";
|
||||
card.innerHTML = '<div class="label">' + m.l + '</div><div class="value" style="color:' + m.icon + '">' + m.v + '</div>';
|
||||
mc.appendChild(card);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ======== 2. DISTRIBUTION BAR CHARTS ========
|
||||
var dKeys = C.distributions ? Object.keys(C.distributions) : [];
|
||||
if (dKeys.length > 0) {
|
||||
var dc = document.getElementById("distribution-charts");
|
||||
if (dc) {
|
||||
dKeys.forEach(function(col, idx) {
|
||||
var div = mkDiv(dc, 320);
|
||||
var ch = echarts.init(div);
|
||||
I.push(ch);
|
||||
var dd = C.distributions[col];
|
||||
ch.setOption({
|
||||
title: { text: col, left: "center", top: 8, textStyle: { fontSize: 14, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" } },
|
||||
grid: { left: "3%", right: "4%", bottom: "10%", top: "18%", containLabel: true },
|
||||
xAxis: { type: "category", data: dd.bins, axisLabel: { rotate: 35, fontSize: 9, color: "#94a3b8" }, axisLine: { lineStyle: { color: "#e2e8f0" } } },
|
||||
yAxis: { type: "value", splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } }, axisLabel: { color: "#94a3b8" } },
|
||||
series: [{
|
||||
type: "bar", data: dd.counts, barWidth: "60%",
|
||||
itemStyle: {
|
||||
borderRadius: [6, 6, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: COLORS[idx % COLORS.length] },
|
||||
{ offset: 1, color: COLORS[idx % COLORS.length] + "66" }
|
||||
])
|
||||
},
|
||||
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: "rgba(0,0,0,0.15)" } }
|
||||
}]
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var ds = document.getElementById("distribution-section");
|
||||
if (ds) ds.style.display = "none";
|
||||
}
|
||||
|
||||
// ======== 3. CORRELATION HEATMAP ========
|
||||
var corr = C.correlations;
|
||||
if (corr && corr.cols && corr.cols.length > 1) {
|
||||
var ce = document.getElementById("correlation-chart");
|
||||
if (ce) {
|
||||
var ch = echarts.init(ce);
|
||||
I.push(ch);
|
||||
ch.setOption({
|
||||
tooltip: {
|
||||
position: "top",
|
||||
backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" },
|
||||
formatter: function(p) { return corr.cols[p.data[0]] + " vs " + corr.cols[p.data[1]] + "<br/>r = <b>" + p.data[2] + "</b>"; }
|
||||
},
|
||||
grid: { height: "68%", top: "5%", bottom: "22%", left: "15%", right: "5%" },
|
||||
xAxis: { type: "category", data: corr.cols, splitArea: { show: true, areaStyle: { color: ["rgba(250,250,250,0.3)", "rgba(200,200,200,0.08)"] } }, axisLabel: { interval: 0, rotate: 35, fontSize: 11, color: "#64748b" } },
|
||||
yAxis: { type: "category", data: corr.cols, splitArea: { show: true, areaStyle: { color: ["rgba(250,250,250,0.3)", "rgba(200,200,200,0.08)"] } }, axisLabel: { fontSize: 11, color: "#64748b" } },
|
||||
visualMap: { min: -1, max: 1, calculable: true, orient: "horizontal", left: "center", bottom: "2%", itemWidth: 20, itemHeight: 140, textStyle: { color: "#64748b" }, inRange: { color: ["#312e81","#4338ca","#6366f1","#a5b4fc","#e0e7ff","#fef3c7","#fcd34d","#f59e0b","#ea580c","#dc2626","#991b1b"] } },
|
||||
series: [{
|
||||
type: "heatmap", data: corr.data,
|
||||
label: { show: true, fontSize: 11, fontWeight: 600, formatter: function(p) { return p.data[2].toFixed(2); } },
|
||||
emphasis: { itemStyle: { shadowBlur: 12, shadowColor: "rgba(0,0,0,0.25)" } }
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var cs = document.getElementById("correlation-section");
|
||||
if (cs) cs.style.display = "none";
|
||||
}
|
||||
|
||||
// ======== 4. CATEGORICAL PIE/DONUT CHARTS ========
|
||||
var catKeys = C.categories ? Object.keys(C.categories) : [];
|
||||
if (catKeys.length > 0) {
|
||||
var cc = document.getElementById("categorical-charts");
|
||||
if (cc) {
|
||||
catKeys.forEach(function(col, idx) {
|
||||
var div = mkDiv(cc, 380);
|
||||
var ch = echarts.init(div);
|
||||
I.push(ch);
|
||||
var pd = C.categories[col].labels.map(function(lbl, i) { return { name: lbl, value: C.categories[col].values[i] }; });
|
||||
ch.setOption({
|
||||
title: { text: col, left: "center", top: 8, textStyle: { fontSize: 14, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { trigger: "item", formatter: "{b}<br/>{c} ({d}%)", backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" } },
|
||||
legend: { orient: "horizontal", bottom: 0, type: "scroll", textStyle: { fontSize: 11, color: "#64748b" } },
|
||||
color: COLORS,
|
||||
series: [{
|
||||
type: "pie", radius: ["42%", "68%"], center: ["50%", "48%"],
|
||||
data: pd,
|
||||
label: { show: true, formatter: "{d}%", fontSize: 11, fontWeight: 600 },
|
||||
itemStyle: { borderRadius: 6, borderColor: "#fff", borderWidth: 2 },
|
||||
emphasis: { scaleSize: 8, label: { fontSize: 14, fontWeight: 700 } }
|
||||
}]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var bestCat = catKeys[0];
|
||||
var topnEl = document.getElementById("topn-chart");
|
||||
if (topnEl && C.categories[bestCat]) {
|
||||
var td = C.categories[bestCat];
|
||||
var labels = td.labels.slice().reverse();
|
||||
var vals = td.values.slice().reverse();
|
||||
var ch = echarts.init(topnEl);
|
||||
I.push(ch);
|
||||
ch.setOption({
|
||||
title: { text: bestCat + " Top " + labels.length, left: "center", top: 8, textStyle: { fontSize: 14, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" } },
|
||||
grid: { left: "3%", right: "10%", bottom: "5%", top: "15%", containLabel: true },
|
||||
xAxis: { type: "value", splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } }, axisLabel: { color: "#94a3b8" } },
|
||||
yAxis: { type: "category", data: labels, axisLabel: { fontSize: 11, color: "#334155", width: 120, overflow: "truncate" } },
|
||||
series: [{
|
||||
type: "bar", data: vals, barWidth: "55%",
|
||||
itemStyle: { borderRadius: [0, 6, 6, 0], color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [{ offset: 0, color: "#818cf8" }, { offset: 1, color: "#6366f1" }]) },
|
||||
label: { show: true, position: "right", formatter: "{c}", fontSize: 11, fontWeight: 600, color: "#4f46e5" }
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var cas = document.getElementById("categorical-section");
|
||||
if (cas) cas.style.display = "none";
|
||||
}
|
||||
|
||||
// ======== 5. TIME SERIES LINE CHART ========
|
||||
var ts = C.time_series;
|
||||
if (ts && ts.dates && ts.dates.length > 0) {
|
||||
var te = document.getElementById("time-series-chart");
|
||||
if (te) {
|
||||
var ch = echarts.init(te);
|
||||
I.push(ch);
|
||||
ch.setOption({
|
||||
title: { text: (ts.name || "") + " Trend", left: "center", top: 8, textStyle: { fontSize: 14, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { trigger: "axis", backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" } },
|
||||
grid: { left: "3%", right: "4%", bottom: "8%", top: "18%", containLabel: true },
|
||||
xAxis: { type: "category", data: ts.dates, boundaryGap: false, axisLabel: { color: "#94a3b8", rotate: 30 }, axisLine: { lineStyle: { color: "#e2e8f0" } } },
|
||||
yAxis: { type: "value", scale: true, splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } }, axisLabel: { color: "#94a3b8" } },
|
||||
dataZoom: [{ type: "inside", start: 0, end: 100 }],
|
||||
series: [{
|
||||
type: "line", data: ts.values, smooth: true, symbol: "circle", symbolSize: 6,
|
||||
areaStyle: { opacity: 0.15, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: "#8b5cf6" }, { offset: 1, color: "#8b5cf600" }]) },
|
||||
itemStyle: { color: "#8b5cf6" },
|
||||
lineStyle: { width: 3, shadowColor: "rgba(139,92,246,0.3)", shadowBlur: 8, shadowOffsetY: 4 }
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
var tss = document.getElementById("time-series-section");
|
||||
if (tss) tss.style.display = "none";
|
||||
}
|
||||
|
||||
// ======== 6. RADAR CHART ========
|
||||
var numCols = C.numeric_cols || [];
|
||||
var dists = C.distributions || {};
|
||||
var radarEl = document.getElementById("radar-chart");
|
||||
if (radarEl && numCols.length >= 3) {
|
||||
var radarCols = numCols.slice(0, 8);
|
||||
var indicator = [];
|
||||
var radarValues = [];
|
||||
radarCols.forEach(function(col) {
|
||||
var d = dists[col];
|
||||
if (d && d.counts) {
|
||||
var total = d.counts.reduce(function(a, b) { return a + b; }, 0);
|
||||
indicator.push({ name: col, max: total });
|
||||
var maxVal = Math.max.apply(null, d.counts);
|
||||
radarValues.push(maxVal);
|
||||
}
|
||||
});
|
||||
if (indicator.length >= 3) {
|
||||
var ch = echarts.init(radarEl);
|
||||
I.push(ch);
|
||||
ch.setOption({
|
||||
title: { text: "Feature Distribution Radar", left: "center", top: 8, textStyle: { fontSize: 13, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" } },
|
||||
radar: {
|
||||
indicator: indicator, shape: "polygon",
|
||||
axisName: { color: "#64748b", fontSize: 10 },
|
||||
splitArea: { areaStyle: { color: ["#fff", "#f8fafc", "#f1f5f9", "#e2e8f0"] } },
|
||||
splitLine: { lineStyle: { color: "#e2e8f0" } }
|
||||
},
|
||||
series: [{
|
||||
type: "radar",
|
||||
data: [{ value: radarValues, name: "Peak Frequency", areaStyle: { opacity: 0.2, color: "#6366f1" }, lineStyle: { color: "#6366f1", width: 2 }, itemStyle: { color: "#6366f1" } }]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (radarEl && numCols.length < 3) { radarEl.style.display = "none"; }
|
||||
|
||||
// ======== 7. SCATTER CHART ========
|
||||
var scatterEl = document.getElementById("scatter-chart");
|
||||
if (scatterEl && C.scatter && C.scatter.x && C.scatter.x.length > 0) {
|
||||
var sc = C.scatter;
|
||||
var scData = sc.x.map(function(xv, i) { return [xv, sc.y[i]]; });
|
||||
var ch = echarts.init(scatterEl);
|
||||
I.push(ch);
|
||||
ch.setOption({
|
||||
title: { text: sc.x_name + " vs " + sc.y_name, left: "center", top: 8, textStyle: { fontSize: 13, fontWeight: 600, color: "#334155" } },
|
||||
tooltip: { backgroundColor: "rgba(30,41,59,0.9)", borderWidth: 0, textStyle: { color: "#fff" }, formatter: function(p) { return sc.x_name + ": " + p.data[0] + "<br/>" + sc.y_name + ": " + p.data[1]; } },
|
||||
grid: { left: "10%", right: "5%", bottom: "12%", top: "18%" },
|
||||
xAxis: { type: "value", name: sc.x_name, nameTextStyle: { color: "#94a3b8", fontSize: 11 }, splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } }, axisLabel: { color: "#94a3b8" }, scale: true },
|
||||
yAxis: { type: "value", name: sc.y_name, nameTextStyle: { color: "#94a3b8", fontSize: 11 }, splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } }, axisLabel: { color: "#94a3b8" }, scale: true },
|
||||
series: [{
|
||||
type: "scatter", data: scData, symbolSize: 8,
|
||||
itemStyle: { color: "#f43f5e", opacity: 0.6, borderColor: "#fff", borderWidth: 1 },
|
||||
emphasis: { itemStyle: { opacity: 1, shadowBlur: 8, shadowColor: "rgba(244,63,94,0.4)" } }
|
||||
}]
|
||||
});
|
||||
} else if (scatterEl) {
|
||||
scatterEl.style.display = "none";
|
||||
}
|
||||
|
||||
// Hide advanced section if nothing to show
|
||||
var advSec = document.getElementById("advanced-section");
|
||||
if (advSec) {
|
||||
var radarHidden = !radarEl || radarEl.style.display === "none";
|
||||
var scatterHidden = !scatterEl || scatterEl.style.display === "none";
|
||||
if (radarHidden && scatterHidden) advSec.style.display = "none";
|
||||
}
|
||||
|
||||
// ======== 8. STATS TABLE ========
|
||||
var tableEl = document.getElementById("stats-table-container");
|
||||
var tableSec = document.getElementById("table-section");
|
||||
if (tableEl && C.stats_table && C.stats_table.headers && C.stats_table.rows && C.stats_table.rows.length > 0) {
|
||||
var st = C.stats_table;
|
||||
var html = '<table class="data-table"><thead><tr>';
|
||||
st.headers.forEach(function(h) { html += "<th>" + h + "</th>"; });
|
||||
html += "</tr></thead><tbody>";
|
||||
st.rows.forEach(function(row) {
|
||||
html += "<tr>";
|
||||
row.forEach(function(cell, ci) {
|
||||
var cls = ci === 0 ? "" : (typeof cell === "number" ? ' class="num"' : "");
|
||||
var val = typeof cell === "number" ? cell.toLocaleString() : cell;
|
||||
html += "<td" + cls + ">" + val + "</td>";
|
||||
});
|
||||
html += "</tr>";
|
||||
});
|
||||
html += "</tbody></table>";
|
||||
tableEl.innerHTML = html;
|
||||
} else if (tableSec) {
|
||||
tableSec.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("resize", function() { I.forEach(function(c) { if (c) c.resize(); }); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user