// ===== 全局状态 ===== let chartInstance = null; let parsedData = null; let seriesColors = []; let seriesOrder = []; let seriesAxis = []; // 每个系列所属轴:0=左轴 1=右轴 let currentMode = 'chart'; // 'chart' or 'table' let tableImageBlob = null; // ===== 预设颜色方案 ===== const colorPalettes = { default: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'], dark: ['#4fc3f7', '#81c784', '#fff176', '#ff8a65', '#ba68c8', '#4dd0e1', '#ffab91', '#aed581', '#f48fb1'], macarons: ['#2ec7c9', '#b6a2de', '#5ab1ef', '#ffb980', '#d87a80', '#8d98b3', '#e5cf0d', '#97b552', '#95706d'], gradient: ['#7f7fd5', '#86a8e7', '#91eae4', '#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff', '#5f27cd'], retro: ['#d4a5a5', '#95b9c7', '#f6e8c3', '#dfceb4', '#a4c3b5', '#c9b1ff', '#f5c7b8', '#b8d4e3', '#e8c8a0'], ocean: ['#0077b6', '#00b4d8', '#90e0ef', '#caf0f8', '#023e8a', '#48cae4', '#ade8f4', '#0096c7', '#03045e'], forest: ['#2d6a4f', '#40916c', '#52b788', '#74c69d', '#95d5b2', '#b7e4c7', '#d8f3dc', '#1b4332', '#344e41'], sunset: ['#e85d04', '#f48c06', '#faa307', '#ffba08', '#dc2f02', '#d00000', '#9d0208', '#6a040f', '#370617'], lavender: ['#7c3aed', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe', '#ede9fe', '#5b21b6', '#4c1d95', '#6d28d9'], minimal: ['#111827', '#374151', '#6b7280', '#9ca3af', '#d1d5db', '#e5e7eb', '#f3f4f6', '#4b5563', '#1f2937'], cherry: ['#e91e63', '#f06292', '#f48fb1', '#f8bbd0', '#fce4ec', '#c2185b', '#ad1457', '#880e4f', '#d81b60'], midnight: ['#6c63ff', '#5a52d5', '#483fb5', '#7c79ff', '#3d3d5c', '#8b89ff', '#9d9bff', '#2c2c4a', '#b3b2ff'], gold: ['#b8860b', '#daa520', '#f0c040', '#ffd700', '#ffe066', '#996515', '#cc9900', '#e6b800', '#8b6914'], coral: ['#ff6f61', '#ff8a70', '#ffa07a', '#ffab91', '#ffccbc', '#e64a19', '#f4511e', '#ff5722', '#d84315'], mint: ['#00bfa5', '#26a69a', '#4db6ac', '#80cbc4', '#b2dfdb', '#00897b', '#00796b', '#004d40', '#009688'] }; // ===== 示例数据 ===== const sampleDataSets = [ { name: '季度销售对比', data: `产品, Q1, Q2, Q3, Q4 手机, 1200, 1800, 2100, 2500 平板, 800, 950, 1100, 1300 笔记本, 600, 750, 900, 1050 耳机, 400, 520, 680, 800` }, { name: '年度增长趋势', data: `指标, 2020年, 2021年, 2022年, 2023年, 2024年 营收(万), 500, 680, 820, 1050, 1380 利润(万), 80, 120, 160, 230, 350 用户(千), 50, 85, 130, 200, 320` }, { name: '多指标对比(适合区域分割)', data: `月份, 方案A-效率, 方案A-成本, 方案B-效率, 方案B-成本 1月, 85, 120, 78, 135 2月, 88, 115, 82, 128 3月, 92, 108, 88, 120 4月, 90, 112, 95, 110 5月, 95, 105, 98, 105 6月, 98, 98, 102, 95` } ]; // ===== 初始化 ===== document.addEventListener('DOMContentLoaded', () => { // 默认加载第一个示例 document.getElementById('dataInput').value = sampleDataSets[0].data; generateChart(); // 预填双图合并示例(不提前生成,切到双图模式时由 switchMode 触发) document.getElementById('combineData1').value = sampleDataSets[0].data; document.getElementById('combineData2').value = sampleDataSets[1].data; document.getElementById('combineTitle1').value = '季度销售对比'; document.getElementById('combineType2').value = 'line'; document.getElementById('combineTitle2').value = '年度增长趋势'; // 初始化导出设置 renderHistory(); refreshExportHint(); }); // ===== 数据解析 ===== function parseData(rawText) { const lines = rawText.trim().split('\n').filter(l => l.trim()); if (lines.length < 2) { alert('数据至少需要包含表头和一行数据'); return null; } // 自动检测分隔符 let delimiter = ','; if (lines[0].includes('\t')) { delimiter = '\t'; } else if (lines[0].split('|').length > lines[0].split(',').length) { delimiter = '|'; } const rows = lines.map(line => { return line.split(delimiter).map(cell => cell.trim()); }); const headers = rows[0]; const categories = []; const seriesData = {}; // 第一列是横坐标,其余列是系列 for (let i = 1; i < headers.length; i++) { seriesData[headers[i]] = []; } for (let i = 1; i < rows.length; i++) { categories.push(rows[i][0]); for (let j = 1; j < rows[i].length && j < headers.length; j++) { const val = parseFloat(rows[i][j]); seriesData[headers[j]].push(isNaN(val) ? 0 : val); } } return { categories, seriesNames: headers.slice(1), seriesData }; } // ===== 生成图表 ===== function generateChart() { const rawText = document.getElementById('dataInput').value; if (!rawText.trim()) { alert('请输入数据'); return; } parsedData = parseData(rawText); if (!parsedData) return; // 初始化系列顺序和颜色 seriesOrder = parsedData.seriesNames.map((_, i) => i); const palette = colorPalettes[document.getElementById('themeStyle').value] || colorPalettes.default; seriesColors = parsedData.seriesNames.map((_, i) => palette[i % palette.length]); seriesAxis = parsedData.seriesNames.map(() => 0); // 渲染系列配置 renderSeriesConfig(); // 初始化图表 initChart(); updateChart(); refreshPreview(); } // ===== 初始化图表实例 ===== function initChart() { const chartDom = document.getElementById('chartArea'); chartDom.innerHTML = ''; if (chartInstance) { chartInstance.dispose(); } chartInstance = echarts.init(chartDom, null, { renderer: 'canvas' }); // 响应式 window.addEventListener('resize', () => { chartInstance && chartInstance.resize(); }); } // ===== 更新图表 ===== function updateChart() { if (!parsedData || !chartInstance) return; const chartType = document.getElementById('chartType').value; const title = document.getElementById('chartTitle').value; const theme = document.getElementById('themeStyle').value; const showLegend = document.getElementById('showLegend').checked; const showGrid = document.getElementById('showGrid').checked; const showLabel = document.getElementById('showLabel').checked; const stackMode = document.getElementById('stackMode').checked; const smoothLine = document.getElementById('smoothLine').checked; const enableSplit = document.getElementById('enableSplit').checked; const dualAxis = document.getElementById('dualAxis').checked; const rightAxisName = document.getElementById('rightAxisName').value; // 显示/隐藏分割配置 document.getElementById('splitConfig').style.display = enableSplit ? 'block' : 'none'; // 背景色 const bgColor = theme === 'dark' ? '#1a1a2e' : '#ffffff'; const textColor = theme === 'dark' ? '#e0e0e0' : '#333333'; const axisLineColor = theme === 'dark' ? '#444' : '#ddd'; // 按当前顺序构建系列 const palette = colorPalettes[theme] || colorPalettes.default; const series = seriesOrder.map((origIdx, displayIdx) => { const name = parsedData.seriesNames[origIdx]; const data = parsedData.seriesData[name]; const color = seriesColors[origIdx] || palette[origIdx % palette.length]; let type = chartType === 'bar-line' ? (displayIdx % 2 === 0 ? 'bar' : 'line') : chartType; const seriesItem = { name: name, type: type, data: [...data], itemStyle: { color: color }, emphasis: { focus: 'series', itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } } }; // 堆叠 if (stackMode) { seriesItem.stack = 'total'; } // 折线图特有 if (type === 'line') { seriesItem.smooth = smoothLine; seriesItem.lineStyle = { width: 3 }; seriesItem.symbolSize = 8; seriesItem.areaStyle = theme === 'gradient' ? { opacity: 0.15 } : undefined; } // 柱状图特有 if (type === 'bar') { seriesItem.barMaxWidth = 40; seriesItem.itemStyle.borderRadius = stackMode ? [0, 0, 0, 0] : [4, 4, 0, 0]; if (theme === 'gradient') { seriesItem.itemStyle.color = new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: color }, { offset: 1, color: adjustColor(color, 40) } ]); } } // 数据标签 if (showLabel) { seriesItem.label = { show: true, position: type === 'bar' ? 'top' : 'top', fontSize: 11, color: textColor, formatter: (params) => { if (params.value >= 10000) return (params.value / 10000).toFixed(1) + 'w'; if (params.value >= 1000) return (params.value / 1000).toFixed(1) + 'k'; return params.value; } }; } // 双Y轴:按系列指定左右轴 if (dualAxis) { seriesItem.yAxisIndex = (seriesAxis[origIdx] === 1) ? 1 : 0; } return seriesItem; }); // 区域分割 - 使用 graphic 组件精确画分割线在柱子间隙中 const graphicElements = []; if (enableSplit) { const splitIdx = parseInt(document.getElementById('splitIndex').value) || 3; const leftLabel = document.getElementById('leftLabel').value || '左侧'; const rightLabel = document.getElementById('rightLabel').value || '右侧'; const splitStyle = document.getElementById('splitStyle').value; if (splitIdx < parsedData.categories.length) { // 区域背景色用 markArea const splitLineColor = theme === 'dark' ? '#ff6b6b' : '#e74c3c'; if (series.length > 0) { series[0].markArea = { silent: true, data: [ [ { name: leftLabel, xAxis: parsedData.categories[0], itemStyle: { color: theme === 'dark' ? 'rgba(79,195,247,0.06)' : 'rgba(79,70,229,0.05)', borderWidth: 0 }, label: { show: true, position: 'insideTop', fontSize: 13, fontWeight: 'bold', color: theme === 'dark' ? '#4fc3f7' : '#4f46e5', offset: [0, 10] } }, { xAxis: parsedData.categories[splitIdx - 1] } ], [ { name: rightLabel, xAxis: parsedData.categories[splitIdx], itemStyle: { color: theme === 'dark' ? 'rgba(255,107,107,0.06)' : 'rgba(239,68,68,0.05)', borderWidth: 0 }, label: { show: true, position: 'insideTop', fontSize: 13, fontWeight: 'bold', color: theme === 'dark' ? '#ff6b6b' : '#ef4444', offset: [0, 10] } }, { xAxis: parsedData.categories[parsedData.categories.length - 1] } ] ] }; } // 用 graphic 组件在渲染后画分割线(在柱子间隙中) // 先存到全局,在 setOption 后通过 rendered 事件绘制 window._splitConfig = { splitIdx, splitLineColor, splitStyle, theme }; } else { window._splitConfig = null; } } else { window._splitConfig = null; } // 图表配置 const option = { backgroundColor: bgColor, title: title ? { text: title, left: 'center', top: 15, textStyle: { color: textColor, fontSize: 18, fontWeight: 600 } } : undefined, tooltip: { trigger: 'axis', backgroundColor: theme === 'dark' ? 'rgba(30,30,50,0.95)' : 'rgba(255,255,255,0.95)', borderColor: theme === 'dark' ? '#555' : '#eee', textStyle: { color: textColor }, axisPointer: { type: chartType === 'bar' ? 'shadow' : 'cross', crossStyle: { color: '#999' } }, formatter: function(params) { let html = `
📊
输入数据后点击"生成图表"
生成图表后可在此调整各系列的顺序和颜色
'; } // ===== 导出图表(SVG 专用) ===== function exportChartSvg() { if (!chartInstance) { alert('请先生成图表'); return; } const svgChart = echarts.init(document.createElement('div'), null, { renderer: 'svg' }); svgChart.setOption(chartInstance.getOption()); const url = svgChart.getDataURL({ type: 'svg', pixelRatio: 2 }); const a = document.createElement('a'); a.href = url; a.download = buildExportFilename('chart', 'svg'); a.click(); svgChart.dispose(); } // ===== 导出设置(分辨率/历史/命名) ===== const EXPORT_PRESETS = { 'original': { label: '原始大小', w: 0, h: 0 }, '1920x1080': { label: '1920 × 1080', w: 1920, h: 1080 }, '1280x720': { label: '1280 × 720', w: 1280, h: 720 }, '1024x768': { label: '1024 × 768', w: 1024, h: 768 }, '800x600': { label: '800 × 600', w: 800, h: 600 }, 'custom': { label: '自定义', w: null, h: null } }; const HISTORY_KEY = 'dct_export_history'; const HISTORY_MAX = 5; // 当前生效的分辨率 {w, h}(0 表示原始大小) function getResolution() { const preset = document.getElementById('exportPreset').value; if (preset === 'original') return { w: 0, h: 0 }; if (preset === 'custom') { const w = parseInt(document.getElementById('customWidth').value) || 0; const h = parseInt(document.getElementById('customHeight').value) || 0; return { w, h }; } const p = EXPORT_PRESETS[preset]; return { w: p.w, h: p.h }; } function onExportPresetChange() { const preset = document.getElementById('exportPreset').value; document.getElementById('customResGroup').style.display = preset === 'custom' ? 'block' : 'none'; refreshExportHint(); refreshPreview(); } // 导出设置面板展开/折叠(默认折叠) let exportSettingsOpen = false; function toggleExportSettings() { exportSettingsOpen = !exportSettingsOpen; const body = document.getElementById('exportSettingsBody'); const t = document.getElementById('exportToggle'); body.style.display = exportSettingsOpen ? 'block' : 'none'; t.textContent = exportSettingsOpen ? '▾' : '▸'; } // 一键恢复默认(重置全部设置,历史记录保留) function resetExportSettings() { document.getElementById('exportPreset').value = 'original'; document.getElementById('customResGroup').style.display = 'none'; document.getElementById('customWidth').value = ''; document.getElementById('customHeight').value = ''; document.getElementById('exportFilename').value = ''; document.getElementById('exportHistory').value = ''; refreshExportHint(); refreshPreview(); } function onCustomResInput() { refreshExportHint(); refreshPreview(); const { w, h } = getResolution(); if (w > 0 && h > 0) saveHistory(w, h); } // 历史分辨率管理(最多 5 个,存 localStorage) function loadHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch (e) { return []; } } function saveHistory(w, h) { if (!w || !h) return; let hist = loadHistory(); const key = w + 'x' + h; hist = hist.filter(x => x.key !== key); hist.unshift({ key, w, h }); hist = hist.slice(0, HISTORY_MAX); localStorage.setItem(HISTORY_KEY, JSON.stringify(hist)); renderHistory(); } function renderHistory() { const sel = document.getElementById('exportHistory'); const hist = loadHistory(); const cur = sel.value; sel.innerHTML = hist.length ? hist.map(h => ``).join('') : ''; if (cur && hist.some(h => h.key === cur)) sel.value = cur; } function onHistorySelect() { const v = document.getElementById('exportHistory').value; if (!v) return; const [w, h] = v.split('x').map(Number); const preset = Object.entries(EXPORT_PRESETS).find(([k, p]) => p.w === w && p.h === h); document.getElementById('exportPreset').value = preset ? preset[0] : 'custom'; if (preset) { document.getElementById('customResGroup').style.display = 'none'; } else { document.getElementById('customResGroup').style.display = 'block'; document.getElementById('customWidth').value = w; document.getElementById('customHeight').value = h; } refreshExportHint(); refreshPreview(); } // 文件名:手动填写优先,否则按日期时间自动命名(每次不同) function buildExportFilename(prefix, ext) { ext = ext || 'png'; const manual = document.getElementById('exportFilename').value.trim(); if (manual) { return /\.[a-zA-Z0-9]+$/.test(manual) ? manual : manual + '.' + ext; } const d = new Date(); const pad = n => String(n).padStart(2, '0'); return `${prefix}_${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}.${ext}`; } // 把源图 contain 居中缩放到目标分辨率画布(自动适配深色背景) function applyResolution(srcCanvas, tw, th) { if (!tw || !th) return srcCanvas; let bg = '#ffffff'; try { const d = srcCanvas.getContext('2d').getImageData(0, 0, 1, 1).data; if (d[3] > 0 && (d[0] + d[1] + d[2]) / 3 < 100) bg = '#1a1a2e'; } catch (e) {} const c = document.createElement('canvas'); c.width = tw; c.height = th; const ctx = c.getContext('2d'); ctx.fillStyle = bg; ctx.fillRect(0, 0, tw, th); const scale = Math.min(tw / srcCanvas.width, th / srcCanvas.height); const dw = Math.round(srcCanvas.width * scale); const dh = Math.round(srcCanvas.height * scale); ctx.drawImage(srcCanvas, Math.round((tw - dw) / 2), Math.round((th - dh) / 2), dw, dh); return c; } // 获取当前模式源 canvas(异步) function getSourceCanvas() { return new Promise((resolve, reject) => { if (currentMode === 'chart') { if (!chartInstance) return reject(new Error('请先生成图表')); const cvs = chartInstance.getDom().querySelector('canvas'); if (!cvs) return reject(new Error('图表未渲染')); resolve(cvs); } else if (currentMode === 'table') { if (!tableImageBlob) return reject(new Error('请先生成表格')); const url = URL.createObjectURL(tableImageBlob); const img = new Image(); img.onload = () => { const c = document.createElement('canvas'); c.width = img.width; c.height = img.height; c.getContext('2d').drawImage(img, 0, 0); URL.revokeObjectURL(url); resolve(c); }; img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('表格图片加载失败')); }; img.src = url; } else { if (!combinedCanvas) return reject(new Error('请先生成合并图')); resolve(combinedCanvas); } }); } // 生成最终导出 canvas(应用当前分辨率) function getExportCanvas() { return getSourceCanvas().then(src => { const { w, h } = getResolution(); return applyResolution(src, w, h); }); } // 统一下载图片 function exportCurrent(prefix) { getExportCanvas().then(canvas => { const a = document.createElement('a'); a.href = canvas.toDataURL('image/png'); a.download = buildExportFilename(prefix); a.click(); const { w, h } = getResolution(); if (w > 0 && h > 0) saveHistory(w, h); }).catch(err => alert(err.message)); } // 导出分辨率提示 + 折叠栏摘要 function refreshExportHint() { const { w, h } = getResolution(); const hint = document.getElementById('exportResHint'); if (hint) hint.textContent = w > 0 && h > 0 ? `导出分辨率:${w} × ${h}(原图居中缩放)` : '导出分辨率:原始大小'; const sum = document.getElementById('exportSummary'); if (sum) sum.textContent = w > 0 && h > 0 ? `${w} × ${h}` : '原始大小'; } // 刷新导出预览:以原图为中心,居中展示保存后的图 function refreshPreview() { const box = document.getElementById('exportPreviewBox'); const inner = document.getElementById('exportPreviewInner'); const title = document.getElementById('exportPreviewTitle'); if (!box || !inner) return; getExportCanvas().then(canvas => { const { w, h } = getResolution(); const url = canvas.toDataURL('image/png'); const sizeTag = w > 0 && h > 0 ? ` ${w} × ${h}` : ''; title.innerHTML = `📐 导出预览${sizeTag}`; inner.innerHTML = `⏳
正在生成表格...
❌
${err.message}
⏳
正在合并...
❌
${err.message}