diff --git a/API.md b/API.md index 986c7f7..614a02d 100644 --- a/API.md +++ b/API.md @@ -32,7 +32,7 @@ Content-Type: application/json | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|:----:|--------|------| | `data` | string | ✅ | — | CSV 格式数据,`\n` 换行,第一行表头,第一列横坐标 | -| `chartType` | string | — | `bar` | 图表类型:`bar` / `line` / `bar-line` | +| `chartType` | string | — | `bar` | 图表类型:`bar` / `line` / `bar-line` / `pie` / `radar` | | `title` | string | — | `""` | 图表标题 | | `theme` | string | — | `default` | 主题风格:`default` / `dark` / `macarons` / `gradient` / `retro` | | `showLegend` | boolean | — | `true` | 显示图例 | @@ -181,7 +181,7 @@ Content-Type: application/json > 兼容旧参数:`chart1` + `chart2` 仍可传(等价于 `charts: [chart1, chart2]`)。 -每个子图配置支持:`data`(CSV)、`chartType`(bar/line/bar-line)、`title`、`theme`、`showLegend`、`showGrid`、`showLabel`、`stackMode`、`smoothLine`、`width`、`height`。 +每个子图配置支持:`data`(CSV)、`chartType`(bar/line/bar-line/pie/radar)、`title`、`theme`、`showLegend`、`showGrid`、`showLabel`、`stackMode`、`smoothLine`、`width`、`height`。 ### curl 示例 diff --git a/README.md b/README.md index 7c6c502..20aca59 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ - **柱状图** - 支持单系列/多系列对比 - **折线图** - 支持平滑曲线、面积填充 - **混合图** - 柱状图+折线图组合展示 +- **饼图** - 环形占比展示(第一列=名称,第一系列=数值) +- **雷达图** - 多维度对比(第一列=维度,每个系列=一个多边形) - **双Y轴** - 一张图左右两个坐标轴,量度可不同,左右轴可独立命名,系列可指定左轴/右轴且每种类型独立设置(柱状/折线) ### 📋 表格功能 diff --git a/app.js b/app.js index ebe4717..b63fb2a 100644 --- a/app.js +++ b/app.js @@ -157,6 +157,91 @@ function initChart() { }); } +// ===== 饼图/雷达图 option 构建 ===== +function buildPieRadarOption(chartType) { + if (!parsedData || parsedData.seriesNames.length === 0) return null; + const title = document.getElementById('chartTitle').value; + const theme = document.getElementById('themeStyle').value; + const showLegend = document.getElementById('showLegend').checked; + const showLabel = document.getElementById('showLabel').checked; + const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff'; + const textColor = theme === 'dark' ? '#e0e0e0' : '#333333'; + const palette = colorPalettes[theme] || colorPalettes.default; + const colors = seriesOrder.map((origIdx, di) => seriesColors[origIdx] || palette[di % palette.length]); + + const baseTitle = title ? { + text: title, left: 'center', top: 10, + textStyle: { color: textColor, fontSize: 18, fontWeight: 600 } + } : undefined; + + if (chartType === 'pie') { + // 饼图:第一列=名称,第一个系列=数值 + const sName = parsedData.seriesNames[0]; + const data = parsedData.seriesData[sName]; + const pieData = parsedData.categories.map((c, i) => ({ name: c, value: data[i] || 0 })); + return { + backgroundColor: bgColor, + title: baseTitle, + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + legend: showLegend ? { + orient: 'vertical', left: 'left', top: title ? 50 : 20, + textStyle: { color: textColor, fontSize: 12 } + } : { show: false }, + color: colors, + series: [{ + type: 'pie', + radius: ['38%', '68%'], + center: ['52%', '55%'], + avoidLabelOverlap: true, + itemStyle: { borderRadius: 6, borderColor: bgColor, borderWidth: 2 }, + label: { show: showLabel, formatter: '{b}: {d}%', color: textColor }, + labelLine: { show: showLabel }, + data: pieData + }] + }; + } else { + // 雷达图:第一列=维度名,每个系列=一个雷达多边形 + const indicator = parsedData.categories.map(c => { + let max = 0; + parsedData.seriesNames.forEach(n => { + parsedData.seriesData[n].forEach(v => { if (v > max) max = v; }); + }); + return { name: c, max: Math.ceil(max * 1.2) || 100 }; + }); + return { + backgroundColor: bgColor, + title: baseTitle, + tooltip: { trigger: 'item' }, + legend: showLegend ? { + orient: 'horizontal', left: 'center', top: title ? 48 : 15, + textStyle: { color: textColor, fontSize: 12 } + } : { show: false }, + color: colors, + radar: { + indicator, + radius: '62%', + center: ['50%', '55%'], + splitNumber: 5, + axisName: { color: textColor, fontSize: 12 }, + splitLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } }, + splitArea: { + areaStyle: { + color: theme === 'dark' ? ['rgba(79,195,247,0.03)', 'rgba(79,195,247,0.06)'] : ['rgba(79,70,229,0.03)', 'rgba(79,70,229,0.06)'] + } + }, + axisLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } } + }, + series: parsedData.seriesNames.map(n => ({ + type: 'radar', + name: n, + data: [{ value: parsedData.seriesData[n], name: n }], + symbolSize: 4, + areaStyle: { opacity: 0.15 } + })) + }; + } +} + // ===== 更新图表 ===== function updateChart() { if (!parsedData || !chartInstance) return; @@ -174,6 +259,16 @@ function updateChart() { const leftAxisName = document.getElementById('leftAxisName').value; const rightAxisName = document.getElementById('rightAxisName').value; + // 饼图/雷达图:走专用构建逻辑(无坐标轴/网格,不支持双轴/堆叠/分割) + if (chartType === 'pie' || chartType === 'radar') { + const option = buildPieRadarOption(chartType); + if (option) { + chartInstance.setOption(option, true); + refreshPreview(); + } + return; + } + // 显示/隐藏分割配置 document.getElementById('splitConfig').style.display = enableSplit ? 'block' : 'none'; @@ -1058,6 +1153,66 @@ function buildCombineChartOption(dataText, cfg) { const axisLineColor = theme === 'dark' ? '#444' : '#ddd'; const palette = colorPalettes[theme] || colorPalettes.default; + // 饼图/雷达图:专用构建逻辑 + if (chartType === 'pie' || chartType === 'radar') { + if (chartType === 'pie') { + const sName = parsed.seriesNames[0]; + const data = parsed.seriesData[sName]; + const pieData = parsed.categories.map((c, i) => ({ name: c, value: data[i] || 0 })); + return { + backgroundColor: bgColor, + title: title ? { text: title, left: 'center', top: 10, textStyle: { color: textColor, fontSize: 16, fontWeight: 600 } } : undefined, + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + legend: showLegend ? { orient: 'vertical', left: 'left', top: title ? 40 : 10, textStyle: { color: textColor, fontSize: 12 } } : { show: false }, + color: palette, + series: [{ + type: 'pie', + radius: ['38%', '68%'], + center: ['55%', '55%'], + itemStyle: { borderRadius: 6, borderColor: bgColor, borderWidth: 2 }, + label: { show: showLabel, formatter: '{b}: {d}%', color: textColor }, + labelLine: { show: showLabel }, + data: pieData + }], + animation: false + }; + } else { + const indicator = parsed.categories.map(c => { + let max = 0; + parsed.seriesNames.forEach(n => { parsed.seriesData[n].forEach(v => { if (v > max) max = v; }); }); + return { name: c, max: Math.ceil(max * 1.2) || 100 }; + }); + return { + backgroundColor: bgColor, + title: title ? { text: title, left: 'center', top: 10, textStyle: { color: textColor, fontSize: 16, fontWeight: 600 } } : undefined, + tooltip: { trigger: 'item' }, + legend: showLegend ? { orient: 'horizontal', left: 'center', top: title ? 38 : 10, textStyle: { color: textColor, fontSize: 12 } } : { show: false }, + color: palette, + radar: { + indicator, + radius: '62%', + center: ['50%', '55%'], + splitNumber: 5, + axisName: { color: textColor, fontSize: 11 }, + splitLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } }, + splitArea: { + areaStyle: { + color: theme === 'dark' ? ['rgba(79,195,247,0.03)', 'rgba(79,195,247,0.06)'] : ['rgba(79,70,229,0.03)', 'rgba(79,70,229,0.06)'] + } + }, + axisLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } } + }, + series: parsed.seriesNames.map(n => ({ + type: 'radar', name: n, + data: [{ value: parsed.seriesData[n], name: n }], + symbolSize: 4, + areaStyle: { opacity: 0.15 } + })), + animation: false + }; + } + } + const series = parsed.seriesNames.map((name, idx) => { const color = palette[idx % palette.length]; let type = chartType === 'bar-line' ? (idx % 2 === 0 ? 'bar' : 'line') : chartType; @@ -1177,7 +1332,7 @@ function initCombineCharts() { renderCombineCharts(); } -const COMBINE_TYPES = [['bar', '柱状图'], ['line', '折线图'], ['bar-line', '柱状图+折线图混合']]; +const COMBINE_TYPES = [['bar', '柱状图'], ['line', '折线图'], ['bar-line', '柱状图+折线图混合'], ['pie', '饼图'], ['radar', '雷达图']]; const COMBINE_THEMES = [['default', '默认'], ['dark', '深色'], ['macarons', '马卡龙'], ['gradient', '渐变'], ['retro', '复古'], ['ocean', '海洋'], ['forest', '森林'], ['sunset', '日落'], ['lavender', '薰衣草'], ['minimal', '极简'], ['cherry', '樱花'], ['midnight', '午夜'], ['gold', '金色'], ['coral', '珊瑚'], ['mint', '薄荷'], ['slate', '石板灰'], ['sky', '天空蓝'], ['rose', '玫瑰红'], ['amber', '琥珀黄'], ['emerald', '翡翠绿'], ['indigo', '靛蓝色'], ['stone', '石灰白']]; function escHtml(str) { diff --git a/index.html b/index.html index 68de493..1ce9d96 100644 --- a/index.html +++ b/index.html @@ -56,6 +56,8 @@ C, 20, 30, 40 + + diff --git a/server.js b/server.js index 2be1282..7ae22ed 100644 --- a/server.js +++ b/server.js @@ -130,6 +130,72 @@ function buildChartOption(params) { const textColor = theme === 'dark' ? '#e0e0e0' : '#333333'; const axisLineColor = theme === 'dark' ? '#444' : '#ddd'; + // 饼图/雷达图:专用构建逻辑(无坐标轴/网格) + if (chartType === 'pie' || chartType === 'radar') { + const bgC = theme === 'dark' ? '#1a1a2e' : '#ffffff'; + const txtC = theme === 'dark' ? '#e0e0e0' : '#333333'; + const tTitle = title ? { text: title, left: 'center', top: 15, textStyle: { color: txtC, fontSize: 18, fontWeight: 600 } } : undefined; + if (chartType === 'pie') { + // 饼图:第一列=名称,第一个系列=数值 + const sName = parsedData.seriesNames[0]; + const data = parsedData.seriesData[sName]; + const pieData = parsedData.categories.map((c, i) => ({ name: c, value: data[i] || 0 })); + return { + backgroundColor: bgC, + title: tTitle, + tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, + legend: showLegend ? { orient: 'vertical', left: 'left', top: title ? 50 : 20, textStyle: { color: txtC, fontSize: 12 } } : { show: false }, + color: seriesColorsArr, + series: [{ + type: 'pie', + radius: ['38%', '68%'], + center: ['52%', '55%'], + itemStyle: { borderRadius: 6, borderColor: bgC, borderWidth: 2 }, + label: { show: showLabel, formatter: '{b}: {d}%', color: txtC }, + labelLine: { show: showLabel }, + data: pieData + }], + animation: false + }; + } else { + // 雷达图:第一列=维度名,每个系列=一个雷达多边形 + const indicator = parsedData.categories.map(c => { + let max = 0; + parsedData.seriesNames.forEach(n => { parsedData.seriesData[n].forEach(v => { if (v > max) max = v; }); }); + return { name: c, max: Math.ceil(max * 1.2) || 100 }; + }); + return { + backgroundColor: bgC, + title: tTitle, + tooltip: { trigger: 'item' }, + legend: showLegend ? { orient: 'horizontal', left: 'center', top: title ? 48 : 15, textStyle: { color: txtC, fontSize: 12 } } : { show: false }, + color: seriesColorsArr, + radar: { + indicator, + radius: '62%', + center: ['50%', '55%'], + splitNumber: 5, + axisName: { color: txtC, fontSize: 12 }, + splitLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } }, + splitArea: { + areaStyle: { + color: theme === 'dark' ? ['rgba(79,195,247,0.03)', 'rgba(79,195,247,0.06)'] : ['rgba(79,70,229,0.03)', 'rgba(79,70,229,0.06)'] + } + }, + axisLine: { lineStyle: { color: theme === 'dark' ? '#444' : '#ddd' } } + }, + series: parsedData.seriesNames.map(n => ({ + type: 'radar', + name: n, + data: [{ value: parsedData.seriesData[n], name: n }], + symbolSize: 4, + areaStyle: { opacity: 0.15 } + })), + animation: false + }; + } + } + // 构建系列 const series = parsedData.seriesNames.map((name, idx) => { const dataArr = parsedData.seriesData[name]; @@ -928,7 +994,7 @@ app.get('/api/health', (req, res) => { res.json({ status: 'ok', service: 'data-chart-tool', - version: '1.13.1', + version: '1.14.0', endpoints: { 'POST /api/chart': '生成图表图片(JSON body)', 'GET /api/chart': '生成图表图片(URL 参数)', @@ -943,7 +1009,7 @@ app.get('/api/health', (req, res) => { app.get('/api/docs', (req, res) => { res.json({ name: '数据可视化图表生成器 API', - version: '1.13.1', + version: '1.14.0', endpoints: [ { method: 'POST', @@ -952,7 +1018,7 @@ app.get('/api/docs', (req, res) => { 'Content-Type': 'application/json', params: { data: { type: 'string', required: true, description: 'CSV 格式数据(第一行表头,第一列横坐标)' }, - chartType: { type: 'string', default: 'bar', options: ['bar', 'line', 'bar-line'], description: '图表类型' }, + chartType: { type: 'string', default: 'bar', options: ['bar', 'line', 'bar-line', 'pie', 'radar'], description: '图表类型' }, title: { type: 'string', default: '', description: '图表标题' }, theme: { type: 'string', default: 'default', options: ['default', 'dark', 'macarons', 'gradient', 'retro', 'ocean', 'forest', 'sunset', 'lavender', 'minimal', 'cherry', 'midnight', 'gold', 'coral', 'mint'], description: '主题风格' }, showLegend: { type: 'boolean', default: true, description: '是否显示图例' },