--- name: Echarts 核心引擎 description: 后台引擎:提供 Echarts 图表生成所需的完整离线库渲染、双击编辑、实时预览、粘贴拦截与 SVG 导出支持。 author: ymjr version: 1.3.1 license: MIT usage: 后台常驻引擎。解决 Excalidraw 原生不支持复杂数据可视化的问题,为 Echarts 图表提供底层 API 支持、实时可视化编辑和画布上的 iframe 渲染能力。 features: - 注入完整的 Apache Echarts 离线渲染内核与实时预览支持 - 彻底修复画布渲染时 contentEl 为空的报错,全面适配最新插件生命周期 - 拦截画布双击事件,双击图元即可无缝唤起高级可视化编辑器进行原地修改 - 自适应双栏编辑器,表格与批量文本页签切换、固定操作栏、实时预览及输入错误提示 - 多系列分组与堆叠、柱线混合与双轴、XY 散点、雷达、漏斗、仪表盘与玫瑰图 - 拦截 onPaste 钩子,支持直接粘贴表格或数据自动唤起生成 - CSV / TSV 表头与引号解析、系列命名配色、坐标轴标题、数据缩放及完整配置往返编辑 - 拦截 renderElementToSvg 支持带图表的画板离线导出 dependencies: - 独立运行,供 Action-Echarts-Manager 动作脚本调用 autorun: true --- /* ```javascript */ var locales = { zh: { ui_title: "ECharts 图表编辑器", ui_editor_hint: "编辑数据与样式,预览确认后再保存到画布。", ui_data: "图表数据", ui_mode_table: "表格", ui_mode_text: "批量文本", ui_color: "颜色", ui_text_hint: "首行可写:类目,收入,成本。后续每行一个类目和各系列数值;支持 CSV 引号、Excel 制表符。", ui_invalid_data: "请提供至少一行完整数据;系列名称须非空且不重复,所有数值须有效。", ui_invalid_json: "配置必须是 JSON 对象。", ui_library_missing: "ECharts 内核未加载,请重新运行核心引擎。", ui_series: "系列", ui_series_name: "系列名称", ui_series_color: "系列颜色", ui_series_type: "系列图形", ui_series_axis: "系列坐标轴", ui_add_series: "添加系列", ui_remove_series: "删除系列", ui_mixed: "柱线混合图", ui_radar: "雷达图", ui_funnel: "漏斗图", ui_gauge: "仪表盘", ui_rose: "玫瑰图", ui_stacked: "堆叠系列", ui_zoom: "数据缩放", ui_axes: "坐标轴标题", ui_x_axis: "X 轴标题", ui_y_axis: "Y 轴标题", ui_left_axis: "左 Y 轴", ui_right_axis: "右 Y 轴", ui_metric: "展示指标", ui_single_metric_hint: "此图展示所选系列;其他系列仍保留,可切回对比图。", ui_min: "量程下限", ui_max: "量程上限", ui_gauge_row: "展示数据行", ui_xy_hint: "格式:类目,X,系列A,系列B。X 与各 Y 列均为数值,支持 CSV 或 Excel 制表符。", ui_invalid_xy: "散点图需要每行一个有效 X 坐标与各系列的 Y 数值。", ui_invalid_type: "不支持的图表类型,请使用自定义 JSON。", ui_invalid_radar: "雷达图至少需要三个维度。", ui_nonnegative: "此图需要非负数值,饼图、玫瑰图和漏斗图还需至少一个正值。", ui_invalid_gauge: "仪表盘要求下限小于上限,且所选数值位于量程内。", ui_sample_mixed: "双轴对比", ui_sample_radar: "多维对比", ui_sample_scatter: "XY 相关性", ui_sample_funnel: "转化漏斗", ui_sample_gauge: "指标进度", notice_save_failed: "图表写入失败,请确认目标画布仍然打开。", ui_chart_type: "图表类型", ui_bar: "柱状图 (Bar)", ui_horizontal_bar: "条形图 (Horizontal Bar)", ui_line: "折线图 (Line)", ui_area: "面积图 (Area)", ui_pie: "饼图 (Pie)", ui_donut: "环形图 (Donut)", ui_scatter: "散点图 (Scatter)", ui_custom: "自定义 JSON (Custom)", ui_chart_title: "图表标题", ui_title_placeholder: "留空则不显示标题", ui_cancel: "取消", ui_generate: "生成/更新图表", ui_theme_color: "🎨 主题色:", ui_preset_colors: "推荐配色:", ui_quick_samples: "快捷模板:", ui_sample_trend: "📈 季度趋势", ui_sample_share: "🥧 份额占比", ui_sample_rank: "🏆 排名对比", ui_show_legend: "图例", ui_show_labels: "数据标签", ui_smooth_line: "平滑曲线", ui_symbol_shape: "图标形状:", ui_empty_circle: "空心圆", ui_circle: "实心圆", ui_rect: "方块", ui_triangle: "三角", ui_diamond: "菱形", ui_text_input_label: "纯文本快捷输入 (类目,数值)", ui_data_panel_label: "数据明细 (可独立配置颜色)", ui_category_name: "类目名", ui_value: "数值", ui_checkbox_color_title: "勾选后可为此项配置独立颜色", ui_delete: "删除行", ui_add_row: "➕ 添加一行", ui_live_preview: "📈 实时交互预览", ui_toggle_preview_theme: "☀️/🌙 切换深浅预览", ui_preview_tip: "悬浮查看数值,点击图例切换显示。", notice_success: "✨ Echarts 图表已更新!", log_svg_render_failed: "[Feature: Echarts] SVG 渲染失败:", log_no_core: "❌ EA_Core 未运行,请先启动引擎!", log_unmounted: "[{id}] 🔌 已卸载", log_mounted: "[{id}] 🚀 挂载完毕", custom_title_fallback: "自定义图表" }, en: { ui_title: "ECharts chart editor", ui_editor_hint: "Edit data and appearance, then save the preview to your canvas.", ui_data: "Chart data", ui_mode_table: "Table", ui_mode_text: "Bulk text", ui_color: "Color", ui_text_hint: "Optional header: Category,Revenue,Cost. One category and all series values per row. CSV quoting and Excel tabs are supported.", ui_invalid_data: "Provide at least one complete row, unique nonempty series names, and valid numeric values.", ui_invalid_json: "The configuration must be a JSON object.", ui_library_missing: "ECharts is not loaded. Run the core engine again.", ui_series: "Series", ui_series_name: "Series name", ui_series_color: "Series color", ui_series_type: "Series chart", ui_series_axis: "Series axis", ui_add_series: "Add series", ui_remove_series: "Remove series", ui_mixed: "Mixed bar / line", ui_radar: "Radar", ui_funnel: "Funnel", ui_gauge: "Gauge", ui_rose: "Rose", ui_stacked: "Stack series", ui_zoom: "Data zoom", ui_axes: "Axis titles", ui_x_axis: "X axis title", ui_y_axis: "Y axis title", ui_left_axis: "Left Y", ui_right_axis: "Right Y", ui_metric: "Displayed metric", ui_single_metric_hint: "This chart displays the selected series; other series are retained for comparison charts.", ui_min: "Minimum", ui_max: "Maximum", ui_gauge_row: "Displayed row", ui_xy_hint: "Format: Category,X,Series A,Series B. X and all Y columns must be numeric. CSV and Excel tabs are supported.", ui_invalid_xy: "Scatter requires a numeric X coordinate and each series Y value on every row.", ui_invalid_type: "Unsupported chart type. Use custom JSON.", ui_invalid_radar: "Radar requires at least three dimensions.", ui_nonnegative: "This chart requires nonnegative values; pie, rose and funnel also require a positive value.", ui_invalid_gauge: "Gauge requires minimum < maximum and a selected value inside that range.", ui_sample_mixed: "Dual-axis", ui_sample_radar: "Radar comparison", ui_sample_scatter: "XY correlation", ui_sample_funnel: "Conversion", ui_sample_gauge: "Progress", notice_save_failed: "Could not write the chart. Check that the target drawing is still open.", ui_chart_type: "Chart Type", ui_bar: "Bar Chart", ui_horizontal_bar: "Horizontal Bar", ui_line: "Line Chart", ui_area: "Area Chart", ui_pie: "Pie Chart", ui_donut: "Donut Chart", ui_scatter: "Scatter Chart", ui_custom: "Custom JSON", ui_chart_title: "Chart Title", ui_title_placeholder: "Leave empty for no title", ui_cancel: "Cancel", ui_generate: "Generate / Update", ui_theme_color: "🎨 Theme Color:", ui_preset_colors: "Presets:", ui_quick_samples: "Quick Templates:", ui_sample_trend: "📈 Trend", ui_sample_share: "🥧 Share", ui_sample_rank: "🏆 Ranking", ui_show_legend: "Legend", ui_show_labels: "Labels", ui_smooth_line: "Smooth", ui_symbol_shape: "Symbol Shape:", ui_empty_circle: "Empty Circle", ui_circle: "Circle", ui_rect: "Rectangle", ui_triangle: "Triangle", ui_diamond: "Diamond", ui_text_input_label: "Quick Text Input (Category,Value)", ui_data_panel_label: "Data Rows (Per-item color override)", ui_category_name: "Category", ui_value: "Value", ui_checkbox_color_title: "Check to enable custom color for this item", ui_delete: "Delete row", ui_add_row: "➕ Add Row", ui_live_preview: "📈 Live Interactive Preview", ui_toggle_preview_theme: "☀️/🌙 Theme Toggle", ui_preview_tip: "Hover for values. Click legend entries to toggle visibility.", notice_success: "✨ Echarts updated successfully!", log_svg_render_failed: "[Feature: Echarts] SVG render failed:", log_no_core: "❌ EA_Core not running, please start engine first!", log_unmounted: "[{id}] 🔌 Unmounted", log_mounted: "[{id}] 🚀 Mounted successfully", custom_title_fallback: "Custom Chart" } }; const SCRIPT_ID = "ymjr.feature.echarts-engine"; //ymjr: Keep one coherent runtime; script-owned EA instances may be destroyed independently. const chartPlugin = ExcalidrawAutomate.plugin; const EchartsCore = {version:'1.3.1',dataSchemaVersion:2}; //ymjr: Migrate legacy single-value data only at the import boundary. EchartsCore.normalizeChartData = (input = {}) => { const labels = [...(input.labels || ['A', 'B', 'C'])]; const source = input.series || [{name:t('ui_series') + ' 1',values:input.values || [100, 200, 150]}]; return { schemaVersion:2, type:input.type || 'bar', chartTitle:input.chartTitle || '', labels, series:source.map((series, i) => ({ name:series.name ?? `${t('ui_series')} ${i + 1}`, values:[...(series.values || [])], type:series.type || (i ? 'line' : 'bar'), color:series.color || '', yAxisIndex:series.yAxisIndex === 1 ? 1 : 0 })), colors:[...(input.colors || [])], xValues:input.xValues ? [...input.xValues] : labels.map((label, i) => String(label).trim() && Number.isFinite(Number(label)) ? Number(label) : i + 1), themeColor:input.themeColor || '#5470c6', showLabel:!!input.showLabel, showLegend:input.showLegend !== false, smoothLine:input.smoothLine !== false, symbolType:input.symbolType || 'emptyCircle', stacked:!!input.stacked, dataZoom:!!input.dataZoom, xAxisName:input.xAxisName || '', yAxisName:input.yAxisName || '', secondaryAxisName:input.secondaryAxisName || '', valueSeriesIndex:input.valueSeriesIndex ?? 0, gaugeRow:input.gaugeRow ?? 0, gaugeMin:input.gaugeMin ?? 0, gaugeMax:input.gaugeMax ?? 100, customOptions:input.customOptions || '' }; }; //ymjr: Strict table parsing keeps invalid cells from becoming fabricated zero values. const chartNumber = value => typeof value === 'string' && /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?$/i.test(value.trim()) && Number.isFinite(Number(value)); EchartsCore.parseTextToData = (text, {xy = false} = {}) => { if (typeof text !== 'string' || !text.trim()) return null; text = text.replace(/^\uFEFF/, ''); const firstLine = text.split(/\r?\n/, 1)[0]; const delimiter = firstLine.includes('\t') ? '\t' : firstLine.includes(',') ? ',' : firstLine.includes(';') ? ';' : firstLine.includes('|') ? '|' : firstLine.includes(':') ? ':' : null; let rows = []; if (!delimiter) { rows = text.split(/\r?\n/).filter(line => line.trim()).map(line => { const match = line.trim().match(/^(.*?)\s+([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)$/i); return match ? [match[1], match[2]] : []; }); } else { let row = [], cell = '', quoted = false, afterQuote = false; for (let i = 0; i < text.length; i++) { const char = text[i]; if (quoted) { if (char === '"') { if (text[i + 1] === '"') { cell += '"'; i++; } else { quoted = false; afterQuote = true; } } else cell += char; } else if (char === '"' && !cell && !afterQuote) { quoted = true; } else if (char === delimiter || char === '\n' || char === '\r') { row.push(cell); cell = ''; afterQuote = false; if (char !== delimiter) { if (row.some(value => value.trim())) rows.push(row); row = []; if (char === '\r' && text[i + 1] === '\n') i++; } } else { if (char === '"' || (afterQuote && char.trim())) return null; if (!afterQuote) cell += char; } } if (quoted) return null; row.push(cell); if (row.some(value => value.trim())) rows.push(row); } const minimum = xy ? 3 : 2; if (!rows.length || rows[0].length < minimum) return null; const width = rows[0].length; if (rows.some(row => row.length !== width)) return null; const start = xy ? 2 : 1; const explicitHeader=[t('ui_category_name'),'Category','类目名','类目'].includes(rows[0][0].trim()); const numeric=rows[0].slice(1).map(chartNumber); if (!explicitHeader && numeric.some(Boolean) && !numeric.every(Boolean)) return null; const hasHeader=explicitHeader || numeric.every(value=>!value); let names; if (hasHeader) names = rows.shift().slice(start).map(value => value.trim()); else names = Array.from({length:width - start}, (_, i) => `${t('ui_series')} ${i + 1}`); if (!rows.length || names.some(name => !name) || new Set(names).size !== names.length || rows.some(row => !row[0].trim() || row.slice(1).some(value => !chartNumber(value)))) return null; const result = { labels:rows.map(row => row[0]), series:names.map((name, i) => ({name,values:rows.map(row => Number(row[start + i]))})) }; if (xy) result.xValues = rows.map(row => Number(row[1])); return result; }; EchartsCore.serializeDataTable = data => { const xy = data.type === 'scatter'; const quote = value => { const text = String(value); return /[",\r\n]/.test(text) || text !== text.trim() ? `"${text.replace(/"/g, '""')}"` : text; }; const rows = [[t('ui_category_name'), ...(xy ? ['X'] : []), ...data.series.map(series => series.name)]]; data.labels.forEach((label, i) => rows.push([ label, ...(xy ? [data.xValues[i]] : []), ...data.series.map(series => series.values[i]) ])); return rows.map(row => row.map(quote).join(',')).join('\n'); }; EchartsCore.validateChartData = data => { const fail = key => { throw new Error(t(key)); }; if (data.type === 'custom') { const option = typeof data.customOptions === 'string' ? JSON.parse(data.customOptions) : data.customOptions; if (!option || Array.isArray(option) || typeof option !== 'object') fail('ui_invalid_json'); return; } if (!['bar','horizontalBar','line','area','pie','donut','scatter','mixed','radar','funnel','gauge','rose'].includes(data.type)) fail('ui_invalid_type'); if (!data.labels?.length || !data.series?.length || data.labels.some(label => typeof label !== 'string' || !label.trim()) || data.series.some(series => !series.name?.trim() || series.values?.length !== data.labels.length || series.values.some(value => !Number.isFinite(value))) || new Set(data.series.map(series => series.name)).size !== data.series.length) fail('ui_invalid_data'); if (data.type === 'scatter' && (data.xValues?.length !== data.labels.length || data.xValues.some(value => !Number.isFinite(value)))) fail('ui_invalid_xy'); if (data.type === 'mixed' && data.series.some(series => !['bar','line','area'].includes(series.type) || ![0,1].includes(series.yAxisIndex))) fail('ui_invalid_data'); if (data.type === 'radar') { if (data.labels.length < 3) fail('ui_invalid_radar'); if (data.series.some(series => series.values.some(value => value < 0))) fail('ui_nonnegative'); } if (['pie','donut','rose','funnel','gauge'].includes(data.type)) { const series = data.series[data.valueSeriesIndex]; if (!series) fail('ui_invalid_data'); if (data.type === 'gauge') { const value = series.values[data.gaugeRow]; if (!Number.isFinite(data.gaugeMin) || !Number.isFinite(data.gaugeMax) || data.gaugeMin >= data.gaugeMax || !Number.isFinite(value) || value < data.gaugeMin || value > data.gaugeMax) fail('ui_invalid_gauge'); } else if (series.values.some(value => value < 0) || !series.values.some(value => value > 0)) fail('ui_nonnegative'); } }; //ymjr: All visual modes consume the same canonical model; no series is discarded on save. EchartsCore.generateEchartsOptions = (data, isDark = false) => { EchartsCore.validateChartData(data); if (data.type === 'custom') return typeof data.customOptions === 'string' ? data.customOptions : JSON.stringify(data.customOptions); const textColor = isDark ? '#ddd' : '#333'; const muted = isDark ? '#aaa' : '#666'; const palette = [data.themeColor, '#91cc75','#fac858','#ee6666','#73c0de','#3ba272','#fc8452','#9a60b4','#ea7ccc']; const cartesian = ['bar','horizontalBar','line','area','mixed','scatter'].includes(data.type); const option = { backgroundColor:'transparent', color:palette, title:{text:data.chartTitle,left:'center',top:6,textStyle:{fontSize:15,color:textColor}}, tooltip:{trigger:cartesian && data.type !== 'scatter' ? 'axis' : 'item',confine:true}, legend:{show:data.showLegend && data.type !== 'gauge',type:'scroll',bottom:4,textStyle:{color:muted,fontSize:11}}, textStyle:{color:textColor} }; if (cartesian) { const horizontal = data.type === 'horizontalBar'; const scatter = data.type === 'scatter'; const second = data.type === 'mixed' && data.series.some(series => series.yAxisIndex === 1); const valueAxis = name => ({ type:'value',name,nameTextStyle:{color:muted},axisLabel:{color:muted}, splitLine:{lineStyle:{color:isDark ? '#ffffff20' : '#e8e8e8',type:'dashed'}} }); const categoryAxis = name => ({type:'category',name,data:data.labels,axisLabel:{color:muted}, nameTextStyle:{color:muted},axisLine:{lineStyle:{color:isDark ? '#666' : '#ccc'}}}); option.grid = {top:data.chartTitle ? 56 : 38,left:18,right:second ? 48 : horizontal&&data.dataZoom ? 42 : 24, bottom:data.dataZoom&&!horizontal ? 76 : data.showLegend ? 42 : 20,containLabel:true}; option.xAxis = horizontal ? valueAxis(data.xAxisName) : scatter ? valueAxis(data.xAxisName) : categoryAxis(data.xAxisName); option.yAxis = horizontal ? categoryAxis(data.yAxisName) : [ valueAxis(data.yAxisName), ...(second ? [{...valueAxis(data.secondaryAxisName),position:'right',splitLine:{show:false}}] : []) ]; if (data.dataZoom) option.dataZoom = [ {type:'inside',...(horizontal ? {yAxisIndex:0} : {xAxisIndex:0})}, {type:'slider',...(horizontal ? {yAxisIndex:0,orient:'vertical',width:18,right:2,top:44,bottom:42} : {xAxisIndex:0,bottom:data.showLegend ? 30 : 8,height:18})} ]; option.series = data.series.map((series, index) => { const visual = data.type === 'mixed' ? series.type : data.type; const type = visual === 'area' ? 'line' : visual === 'horizontalBar' ? 'bar' : visual; const color = series.color || (data.series.length === 1 ? data.themeColor : palette[index % palette.length]); return { name:series.name,type, yAxisIndex:data.type === 'mixed' ? series.yAxisIndex : 0, stack:data.stacked && !scatter && data.type !== 'mixed' ? 'total' : undefined, smooth:data.smoothLine,areaStyle:visual === 'area' ? {opacity:.25} : undefined, symbol:data.symbolType,symbolSize:scatter ? 10 : 6, itemStyle:{color,borderRadius:type === 'bar' && !data.stacked ? 3 : undefined}, label:{show:data.showLabel,position:horizontal ? 'right' : 'top',color:textColor}, data:series.values.map((value, i) => ({ value:scatter ? [data.xValues[i],value] : value, ...(scatter ? {name:data.labels[i]} : {}), ...(data.series.length === 1 && data.colors[i] ? {itemStyle:{color:data.colors[i]}} : {}) })) }; }); } else if (data.type === 'radar') { option.radar = { center:['50%','54%'],radius:'60%', indicator:data.labels.map((name, i) => ({ name,max:Math.max(1, ...data.series.map(series => series.values[i])) * 1.15 })),axisName:{color:muted} }; option.series = [{ type:'radar',label:{show:data.showLabel,color:textColor}, data:data.series.map((series, index) => ({ name:series.name,value:series.values, itemStyle:{color:series.color || palette[index % palette.length]},areaStyle:{opacity:.12} })) }]; } else { const series = data.series[data.valueSeriesIndex]; const items = series.values.map((value, i) => ({ name:data.labels[i],value,...(data.colors[i] ? {itemStyle:{color:data.colors[i]}} : {}) })); if (data.type === 'gauge') { option.series = [{ type:'gauge',min:data.gaugeMin,max:data.gaugeMax, radius:'82%',center:['50%','57%'],progress:{show:true}, itemStyle:{color:series.color || data.themeColor}, axisLabel:{color:muted},title:{color:textColor,fontSize:13,offsetCenter:[0,'60%']}, detail:{color:textColor,fontSize:26,valueAnimation:true,offsetCenter:[0,'25%']}, data:[items[data.gaugeRow]] }]; } else if (data.type === 'funnel') { option.series = [{ name:series.name,type:'funnel',top:data.chartTitle ? 52 : 28,bottom:data.showLegend ? 48 : 24, left:'15%',width:'70%',sort:'descending',gap:3, label:{show:data.showLabel,position:'inside',color:textColor},data:items }]; } else { option.series = [{ name:series.name,type:'pie',roseType:data.type === 'rose' ? 'radius' : undefined, radius:data.type === 'donut' ? ['42%','68%'] : data.type === 'rose' ? ['12%','65%'] : '66%', center:['50%','52%'],label:{show:data.showLabel,color:textColor}, itemStyle:{borderRadius:4,borderColor:isDark ? '#1e1e1e' : '#fff',borderWidth:2},data:items }]; } } return JSON.stringify(option); }; const { Modal } = ea.obsidian; // 安全解析并获取当前活跃的 ExcalidrawView const getActiveExcalidrawView = () => { if (ea.targetView) return ea.targetView; if (window.app?.workspace) { const activeLeaf = window.app.workspace.activeLeaf; if (activeLeaf?.view?.getViewType?.() === "excalidraw") { return activeLeaf.view; } const leaves = window.app.workspace.getLeavesOfType?.("excalidraw"); if (leaves && leaves.length > 0 && leaves[0].view) { return leaves[0].view; } } return null; }; // 获取插件底层的 React 运行实例 (兼容独立窗口与最新插件打包) const getReact = (view) => { if (typeof React !== "undefined" && React) return React; if (typeof window !== "undefined" && window.React) return window.React; if (view?.packages?.react) return view.packages.react; if (ea?.targetView?.packages?.react) return ea.targetView.packages.react; if (typeof ExcalidrawAutomate !== "undefined" && ExcalidrawAutomate.plugin) { if (typeof ExcalidrawAutomate.plugin.getPackage === "function") { try { const pkg = ExcalidrawAutomate.plugin.getPackage(window); if (pkg?.react) return pkg.react; } catch (e) {} } if (ExcalidrawAutomate.plugin.packageManager?.getRuntimePackage) { try { const pkg = ExcalidrawAutomate.plugin.packageManager.getRuntimePackage(); if (pkg?.react) return pkg.react; } catch (e) {} } } return null; }; // 预设推荐配色 const PRESET_PALETTES = [ { name: "经典蓝", color: "#5470c6" }, { name: "清新绿", color: "#3ba272" }, { name: "活力橙", color: "#fa541c" }, { name: "优雅紫", color: "#722ed1" }, { name: "珊瑚红", color: "#ee6666" }, { name: "青竹碧", color: "#13c2c2" } ]; // 预设快捷数据示例 const QUICK_TEMPLATES = { trend: { type: 'line', title: '季度营收趋势 (万元)', labels: ['第一季度', '第二季度', '第三季度', '第四季度'], series:[{name:'2025',values:[100,150,210,260]}, {name:'2026',values:[120,185,240,310]}] }, share: { type: 'donut', title: '产品市场份额占比', labels: ['产品 A', '产品 B', '产品 C', '其他品类'], values: [42, 28, 18, 12] }, rank: { type: 'horizontalBar', title: '城市业务指标排行', labels: ['杭州', '广州', '深圳', '上海', '北京'], values: [76, 82, 88, 93, 98] }, mixed: { type:'mixed',title:'收入与利润率',labels:['Q1','Q2','Q3','Q4'], yAxisName:'收入',secondaryAxisName:'利润率 %', series:[{name:'收入',type:'bar',values:[120,185,240,310],yAxisIndex:0}, {name:'利润率',type:'line',values:[18,22,21,26],yAxisIndex:1}] }, radar: { type:'radar',title:'能力维度对比',labels:['质量','效率','成本','服务','可靠性'], series:[{name:'方案 A',values:[80,70,65,90,85]},{name:'方案 B',values:[70,90,80,75,80]}] }, scatter: { type:'scatter',title:'投入与产出',labels:['A','B','C','D','E'], xValues:[1,2.5,4,6,8],values:[12,18,25,41,48],xAxisName:'投入',yAxisName:'产出' }, funnel: { type:'funnel',title:'转化流程',labels:['访问','注册','试用','付费'],values:[1000,600,320,180], showLabel:true }, gauge: {type:'gauge',title:'目标达成',labels:['完成率'],values:[76],gaugeMin:0,gaugeMax:100} }; //ymjr: Use scoped CSS and native DOM properties; DomElementInfo ignores style/checked/disabled. class EchartsEditorModal extends Modal { constructor(app, initialData = {}, onSave, onCancel = () => {}) { super(app); this.data = EchartsCore.normalizeChartData(initialData); this.customOptions = initialData.customOptions || '{}'; this.customDirty = !!initialData.customOptions; this.onSave = onSave; this.onCancel = onCancel; this.inputMode = 'table'; this.textDraft = EchartsCore.serializeDataTable(this.data); this.draftDirty = false; this.previewDark = this.contentEl.ownerDocument.body.classList.contains('theme-dark'); } node(parent, tag, cls, text) { const el = parent.ownerDocument.createElement(tag); if (cls) el.className = cls; if (text !== undefined) el.textContent = text; parent.appendChild(el); return el; } button(parent, text, action, cls = '') { const button = this.node(parent, 'button', cls, text); button.type = 'button'; button.onclick = action; return button; } field(parent, text, tag = 'input') { const label = this.node(parent, 'label', 'ec-field'); this.node(label, 'span', '', text); return this.node(label, tag); } onOpen() { this.closed = false; this.modalEl.classList.add('ymjr-ec-modal'); this.contentEl.replaceChildren(); this.contentEl.classList.add('ymjr-ec-editor'); const style = this.node(this.contentEl, 'style'); style.textContent = ` .ymjr-ec-modal {width:1080px;max-width:calc(100vw - 32px);height:760px;max-height:calc(100dvh - 40px);padding:24px;overflow:hidden;container-type:inline-size;} .ymjr-ec-modal .modal-content.ymjr-ec-editor {display:flex;flex-direction:column;gap:18px;min-height:0;height:100%;margin:0;overflow:hidden;font-size:14px;color:var(--text-normal);} .ymjr-ec-editor * {box-sizing:border-box;} .ymjr-ec-editor [hidden] {display:none!important;} .ymjr-ec-editor h2 {margin:0!important;font-size:21px!important;line-height:1.4;color:var(--text-normal)!important;font-weight:650;} .ymjr-ec-editor h3 {margin:0;font-size:14px;font-weight:600;color:var(--text-normal);} .ymjr-ec-editor .ec-muted {font-size:12px;line-height:1.6;color:var(--text-muted);} .ymjr-ec-editor .ec-body {display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:20px;flex:1;min-height:0;overflow:auto;} .ymjr-ec-editor .ec-controls {display:flex;flex-direction:column;gap:16px;min-width:0;overflow:auto;padding:1px 4px 4px 1px;} .ymjr-ec-editor .ec-fields {display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.2fr);gap:12px;} .ymjr-ec-editor .ec-field {display:flex;flex-direction:column;gap:7px;font-size:12px;color:var(--text-muted);min-width:0;} .ymjr-ec-editor input:not([type=checkbox]):not([type=color]),.ymjr-ec-editor select {width:100%;min-width:0;height:34px;padding:6px 9px;font-size:13px;color:var(--text-normal);} .ymjr-ec-editor button {min-height:30px;height:auto;padding:5px 10px;font-size:12px;line-height:1.4;white-space:normal;} .ymjr-ec-editor button:focus-visible {outline:2px solid var(--interactive-accent);outline-offset:2px;} .ymjr-ec-editor .ec-card {border:1px solid var(--background-modifier-border);border-radius:10px;padding:14px;background:var(--background-secondary);} .ymjr-ec-editor .ec-row {display:flex;align-items:center;flex-wrap:wrap;gap:10px;} .ymjr-ec-editor .ec-style {display:flex;flex-direction:column;gap:14px;} .ymjr-ec-editor .ec-swatches {display:flex;align-items:center;gap:9px;flex-wrap:wrap;} .ymjr-ec-editor button.ec-swatch {width:22px;height:22px;min-height:22px;padding:0;border:2px solid var(--background-primary);border-radius:50%;box-shadow:0 0 0 1px var(--background-modifier-border);background:var(--ec-color);} .ymjr-ec-editor button.ec-swatch[aria-pressed=true] {box-shadow:0 0 0 2px var(--interactive-accent);} .ymjr-ec-editor input[type=color] {width:28px;height:28px;padding:2px;border:1px solid var(--background-modifier-border);border-radius:5px;} .ymjr-ec-editor .ec-check {display:inline-flex;align-items:center;gap:7px;font-size:13px;cursor:pointer;white-space:nowrap;} .ymjr-ec-editor input[type=checkbox] {margin:0;flex:none;} .ymjr-ec-editor .ec-data {display:flex;flex-direction:column;gap:12px;flex:1;min-height:230px;} .ymjr-ec-editor .ec-data-head,.ymjr-ec-editor .ec-preview-head {display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;} .ymjr-ec-editor .ec-tabs {display:flex;gap:3px;padding:3px;border-radius:7px;background:var(--background-secondary);} .ymjr-ec-editor .ec-tabs button {box-shadow:none;background:transparent;color:var(--text-muted);border:0;} .ymjr-ec-editor .ec-tabs button[aria-pressed=true] {background:var(--background-primary);color:var(--text-normal);box-shadow:var(--input-shadow);} .ymjr-ec-editor .ec-table-scroll {overflow-x:auto;min-width:0;padding-bottom:6px;} .ymjr-ec-editor .ec-table {display:flex;flex-direction:column;gap:7px;min-width:100%;} .ymjr-ec-editor .ec-table-row {display:grid;grid-template-columns:var(--ec-columns);align-items:center;gap:8px;} .ymjr-ec-editor .ec-table-labels span {overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} .ymjr-ec-editor .ec-series-settings {display:flex;flex-direction:column;gap:10px;} .ymjr-ec-editor .ec-series-list {display:flex;flex-direction:column;gap:8px;} .ymjr-ec-editor .ec-series-row {display:grid;grid-template-columns:minmax(90px,1fr) 48px 30px;gap:8px;align-items:center;} .ymjr-ec-editor .ec-series-row.ec-mixed-row {grid-template-columns:minmax(90px,1fr) 38px 80px 76px 26px;} .ymjr-ec-editor details > summary {cursor:pointer;margin:4px 0;} .ymjr-ec-editor .ec-table-labels {font-size:11px;color:var(--text-muted);} .ymjr-ec-editor .ec-color-cell {display:flex;align-items:center;gap:4px;} .ymjr-ec-editor button.ec-delete {padding:4px;color:var(--text-muted);box-shadow:none;background:transparent;font-size:18px;} .ymjr-ec-editor textarea {display:block;min-height:200px;width:100%;flex:1;resize:vertical;padding:12px;font:13px/1.7 var(--font-monospace);tab-size:2;} .ymjr-ec-editor .ec-preview {display:flex;flex-direction:column;gap:12px;min-width:0;min-height:330px;overflow:hidden;background:var(--background-secondary);border:1px solid var(--background-modifier-border);border-radius:12px;padding:16px;} .ymjr-ec-editor .ec-chart {flex:1;min-height:260px;width:100%;border-radius:7px;overflow:hidden;} .ymjr-ec-editor .ec-footer {display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;gap:10px;flex-shrink:0;border-top:1px solid var(--background-modifier-border);padding-top:14px;} .ymjr-ec-editor .ec-error {color:var(--text-error);font-size:12px;line-height:1.5;overflow-wrap:anywhere;} .ymjr-ec-editor .ec-footer .ec-error {flex-basis:100%;margin-right:auto;} .ymjr-ec-editor .ec-error:empty {display:none;} @container (max-width:740px) { .ymjr-ec-editor .ec-body {grid-template-columns:minmax(0,1fr);align-content:start;} .ymjr-ec-editor .ec-controls {overflow:visible;} .ymjr-ec-editor .ec-preview {height:460px;min-height:460px;} .ymjr-ec-editor .ec-chart {min-height:0;} } @container (max-width:380px) { .ymjr-ec-editor .ec-fields {grid-template-columns:minmax(0,1fr);} .ymjr-ec-editor .ec-series-row.ec-mixed-row {grid-template-columns:minmax(0,1fr) 38px 26px;grid-template-areas:"name color remove" "type axis axis";} .ymjr-ec-editor .ec-mixed-row input:not([type=color]) {grid-area:name;} .ymjr-ec-editor .ec-mixed-row input[type=color] {grid-area:color;} .ymjr-ec-editor .ec-mixed-row select:nth-of-type(1) {grid-area:type;} .ymjr-ec-editor .ec-mixed-row select:nth-of-type(2) {grid-area:axis;} .ymjr-ec-editor .ec-mixed-row button {grid-area:remove;} } `; const header = this.node(this.contentEl, 'header'); this.node(header, 'h2', '', t('ui_title')); this.node(header, 'div', 'ec-muted', t('ui_editor_hint')); const body = this.node(this.contentEl, 'div', 'ec-body'); const controls = this.node(body, 'div', 'ec-controls'); const fields = this.node(controls, 'div', 'ec-fields'); this.typeSelect = this.field(fields, t('ui_chart_type'), 'select'); const types = {bar:'ui_bar',horizontalBar:'ui_horizontal_bar',line:'ui_line',area:'ui_area', pie:'ui_pie',donut:'ui_donut',rose:'ui_rose',scatter:'ui_scatter',mixed:'ui_mixed', radar:'ui_radar',funnel:'ui_funnel',gauge:'ui_gauge',custom:'ui_custom'}; for (const [value, key] of Object.entries(types)) { this.node(this.typeSelect, 'option', '', t(key)).value = value; } this.typeSelect.value = this.data.type; this.typeSelect.onchange = () => { const next = this.typeSelect.value; if (this.data.type !== 'custom' && !this.readTextDraft()) { this.typeSelect.value = this.data.type; this.previewErrorEl.textContent = t('ui_invalid_data'); return; } if (next === 'custom' && !this.customDirty) { try { this.customOptions = JSON.stringify(JSON.parse( EchartsCore.generateEchartsOptions(this.data, this.previewDark)), null, 2); } catch (error) { this.typeSelect.value = this.data.type; this.previewErrorEl.textContent = error.message; return; } } this.data.type = next; this.syncDraft(); this.refreshControls(); }; this.titleInput = this.field(fields, t('ui_chart_title')); this.titleInput.value = this.data.chartTitle; this.titleInput.placeholder = t('ui_title_placeholder'); this.titleInput.oninput = () => { this.data.chartTitle = this.titleInput.value; this.debouncedPreview(); }; this.stylePanel = this.node(controls, 'div', 'ec-card ec-style'); this.sampleBar = this.node(controls, 'div', 'ec-row'); this.node(this.sampleBar, 'span', 'ec-muted', t('ui_quick_samples')); for (const [key, template] of Object.entries(QUICK_TEMPLATES)) { this.button(this.sampleBar, t(`ui_sample_${key}`), () => { this.data = EchartsCore.normalizeChartData({...template,chartTitle:template.title}); this.syncDraft(); this.typeSelect.value = this.data.type; this.titleInput.value = this.data.chartTitle; this.refreshControls(); }); } this.seriesPanel = this.node(controls, 'section', 'ec-card ec-series-settings'); this.editorContainer = this.node(controls, 'section', 'ec-data'); this.previewPanel = this.node(body, 'section', 'ec-preview'); const previewHead = this.node(this.previewPanel, 'div', 'ec-preview-head'); this.node(previewHead, 'h3', '', t('ui_live_preview')); this.button(previewHead, t('ui_toggle_preview_theme'), () => { this.previewDark = !this.previewDark; this.previewChart?.dispose(); this.previewChart = null; this.renderLivePreview(); }); this.previewContainer = this.node(this.previewPanel, 'div', 'ec-chart'); this.node(this.previewPanel, 'div', 'ec-muted', t('ui_preview_tip')); const footer = this.node(this.contentEl, 'footer', 'ec-footer'); this.previewErrorEl = this.node(footer, 'div', 'ec-error'); this.previewErrorEl.setAttribute('role', 'status'); this.button(footer, t('ui_cancel'), () => this.close()); this.saveBtn = this.button(footer, t('ui_generate'), () => this.save(), 'mod-cta'); this.refreshControls(); const win = this.contentEl.ownerDocument.defaultView; this.resizeObserver = new win.ResizeObserver(() => { if (!this.closed) { if (this.previewChart) this.previewChart.resize(); else this.renderLivePreview(); } }); this.resizeObserver.observe(this.previewContainer); } refreshControls() { this.stylePanel.hidden = this.sampleBar.hidden = this.seriesPanel.hidden = this.data.type === 'custom'; this.titleInput.disabled = this.data.type === 'custom'; this.renderStyleSettings(); this.renderSeriesSettings(); this.renderDataEditor(); this.debouncedPreview(); } renderStyleSettings() { this.stylePanel.replaceChildren(); const palette = this.node(this.stylePanel, 'div', 'ec-swatches'); this.node(palette, 'span', 'ec-muted', t('ui_theme_color')); for (const color of PRESET_PALETTES) { const swatch = this.button(palette, '', () => { this.data.themeColor = color.color; this.renderStyleSettings(); this.debouncedPreview(); }, 'ec-swatch'); swatch.style.setProperty('--ec-color', color.color); swatch.setAttribute('aria-label', color.name); swatch.setAttribute('aria-pressed', String(color.color === this.data.themeColor)); } const picker = this.node(palette, 'input'); picker.type = 'color'; picker.value = this.data.themeColor; picker.setAttribute('aria-label', t('ui_theme_color')); picker.oninput = () => { this.data.themeColor = picker.value; for (const swatch of palette.querySelectorAll('button')) swatch.setAttribute('aria-pressed', 'false'); this.debouncedPreview(); }; const toggles = this.node(this.stylePanel, 'div', 'ec-row'); const toggle = (key, text) => { const label = this.node(toggles, 'label', 'ec-check'); const input = this.node(label, 'input'); input.type = 'checkbox'; input.checked = !!this.data[key]; this.node(label, 'span', '', t(text)); input.onchange = () => { this.data[key] = input.checked; this.debouncedPreview(); }; }; toggle('showLegend', 'ui_show_legend'); toggle('showLabel', 'ui_show_labels'); if (['line', 'area', 'mixed'].includes(this.data.type)) toggle('smoothLine', 'ui_smooth_line'); if (['bar','horizontalBar','line','area'].includes(this.data.type)) toggle('stacked','ui_stacked'); if (['bar','horizontalBar','line','area','mixed','scatter'].includes(this.data.type)) toggle('dataZoom','ui_zoom'); if (['line', 'area', 'scatter','mixed'].includes(this.data.type)) { const select = this.field(this.stylePanel, t('ui_symbol_shape'), 'select'); for (const [value, key] of Object.entries({emptyCircle:'ui_empty_circle',circle:'ui_circle', rect:'ui_rect',triangle:'ui_triangle',diamond:'ui_diamond'})) { this.node(select, 'option', '', t(key)).value = value; } select.value = this.data.symbolType; select.onchange = () => { this.data.symbolType = select.value; this.debouncedPreview(); }; } if (['bar','horizontalBar','line','area','mixed','scatter'].includes(this.data.type)) { const axes = this.node(this.stylePanel, 'details'); this.node(axes,'summary','ec-muted',t('ui_axes')); const fields = this.node(axes,'div','ec-fields'); for (const [key, text] of [['xAxisName','ui_x_axis'],['yAxisName','ui_y_axis'], ...(this.data.type === 'mixed' ? [['secondaryAxisName','ui_right_axis']] : [])]) { const input = this.field(fields,t(text)); input.value = this.data[key]; input.oninput = () => {this.data[key]=input.value;this.debouncedPreview();}; } } } renderSeriesSettings() { this.seriesPanel.replaceChildren(); const head = this.node(this.seriesPanel,'div','ec-data-head'); this.node(head,'h3','',t('ui_series')); this.button(head,t('ui_add_series'),() => { if (!this.readTextDraft()) {this.previewErrorEl.textContent=t('ui_invalid_data');return;} let i = this.data.series.length + 1; while (this.data.series.some(series => series.name === `${t('ui_series')} ${i}`)) i++; this.data.series.push({name:`${t('ui_series')} ${i}`,values:this.data.labels.map(()=>0), type:'line',color:'',yAxisIndex:0}); this.syncDraft(); this.renderSeriesSettings(); this.renderDataEditor(); this.debouncedPreview(); }); const list = this.node(this.seriesPanel,'div','ec-series-list'); this.data.series.forEach((series, i) => { const row = this.node(list,'div',`ec-series-row${this.data.type === 'mixed' ? ' ec-mixed-row' : ''}`); const name = this.node(row,'input'); name.value = series.name; name.setAttribute('aria-label',`${t('ui_series_name')} ${i + 1}`); name.oninput = () => { series.name=name.value; this.syncDraft(); const offset=this.data.type==='scatter'?2:1; const heading=this.editorContainer.querySelector('.ec-table-labels')?.children[i+offset]; if (heading) {heading.textContent=name.value;heading.title=name.value;} this.debouncedPreview(); }; const color = this.node(row,'input'); color.type='color'; color.value=series.color || (i ? ['#5470c6','#91cc75','#fac858','#ee6666'][i % 4] : this.data.themeColor); color.setAttribute('aria-label',`${t('ui_series_color')} ${i + 1}`); color.oninput = () => {series.color=color.value;this.debouncedPreview();}; if (this.data.type === 'mixed') { const type = this.node(row,'select'); for (const value of ['bar','line','area']) this.node(type,'option','',t(`ui_${value}`)).value=value; type.value=series.type; type.setAttribute('aria-label',`${t('ui_series_type')} ${i + 1}`); type.onchange = () => {series.type=type.value;this.debouncedPreview();}; const axis = this.node(row,'select'); this.node(axis,'option','',t('ui_left_axis')).value='0'; this.node(axis,'option','',t('ui_right_axis')).value='1'; axis.value=String(series.yAxisIndex); axis.setAttribute('aria-label',`${t('ui_series_axis')} ${i + 1}`); axis.onchange = () => {series.yAxisIndex=Number(axis.value);this.debouncedPreview();}; } const remove = this.button(row,'×',() => { if (!this.readTextDraft()) {this.previewErrorEl.textContent=t('ui_invalid_data');return;} this.data.series.splice(i,1); this.data.valueSeriesIndex=Math.min(this.data.valueSeriesIndex,this.data.series.length-1); this.syncDraft(); this.renderSeriesSettings(); this.renderDataEditor(); this.debouncedPreview(); },'ec-delete'); remove.disabled=this.data.series.length===1; remove.setAttribute('aria-label',`${t('ui_remove_series')} ${i + 1}`); }); if (['pie','donut','rose','funnel','gauge'].includes(this.data.type)) { const select = this.field(this.seriesPanel,t('ui_metric'),'select'); this.data.series.forEach((series,i)=>{this.node(select,'option','',series.name).value=String(i);}); select.value=String(this.data.valueSeriesIndex); select.onchange=()=>{this.data.valueSeriesIndex=Number(select.value);this.debouncedPreview();}; this.node(this.seriesPanel,'div','ec-muted',t('ui_single_metric_hint')); } if (this.data.type === 'gauge') { const fields = this.node(this.seriesPanel,'div','ec-fields'); for (const [key,label] of [['gaugeMin','ui_min'],['gaugeMax','ui_max']]) { const input=this.field(fields,t(label)); input.type='number';input.step='any';input.value=this.data[key]; input.oninput=()=>{this.data[key]=input.valueAsNumber;this.debouncedPreview();}; } const row=this.field(this.seriesPanel,t('ui_gauge_row'),'select'); this.data.labels.forEach((label,i)=>{this.node(row,'option','',label).value=String(i);}); row.value=String(this.data.gaugeRow); row.onchange=()=>{this.data.gaugeRow=Number(row.value);this.debouncedPreview();}; } } renderDataEditor() { this.editorContainer.replaceChildren(); const head = this.node(this.editorContainer, 'div', 'ec-data-head'); this.node(head, 'h3', '', this.data.type === 'custom' ? 'ECharts Option JSON' : t('ui_data')); if (this.data.type !== 'custom') { const tabs = this.node(head, 'div', 'ec-tabs'); for (const mode of ['table', 'text']) { const button = this.button(tabs, t(`ui_mode_${mode}`), () => { if (mode === 'table' && !this.readTextDraft()) { this.previewErrorEl.textContent=t('ui_invalid_data');return; } this.inputMode = mode; this.renderSeriesSettings(); this.renderDataEditor(); }); button.setAttribute('aria-pressed', String(mode === this.inputMode)); } } if (this.data.type === 'custom' || this.inputMode === 'text') { const area = this.node(this.editorContainer, 'textarea'); area.setAttribute('aria-label', this.data.type === 'custom' ? 'ECharts Option JSON' : t('ui_text_input_label')); area.spellcheck = false; area.value = this.data.type === 'custom' ? this.customOptions : this.textDraft; area.oninput = () => { if (this.data.type === 'custom') {this.customOptions=area.value;this.customDirty=true;} else {this.textDraft=area.value;this.draftDirty=true;} this.debouncedPreview(); }; if (this.data.type !== 'custom') this.node(this.editorContainer,'div','ec-muted', t(this.data.type === 'scatter' ? 'ui_xy_hint' : 'ui_text_hint')); return; } const xy = this.data.type === 'scatter'; const perItem = this.data.series.length === 1 || ['pie','donut','rose','funnel'].includes(this.data.type); const scroll = this.node(this.editorContainer,'div','ec-table-scroll'); const table = this.node(scroll, 'div', 'ec-table'); const seriesColumns = this.data.series.map(()=> '82px').join(' '); table.style.setProperty('--ec-columns',`minmax(120px,1fr) ${xy ? '82px' : ''} ${seriesColumns} ${perItem ? '54px' : ''} 30px`); table.style.minWidth=`${120+(this.data.series.length+(xy?1:0))*90+(perItem?62:0)+38}px`; const heading = this.node(table, 'div', 'ec-table-row ec-table-labels'); for (const label of [t('ui_category_name'),...(xy?['X']:[]),...this.data.series.map(s=>s.name),...(perItem?[t('ui_color')]:[])]) { const column=this.node(heading,'span','',label);column.title=label; } this.data.labels.forEach((label, i) => { const row = this.node(table, 'div', 'ec-table-row'); const name = this.node(row, 'input'); name.value = label; name.setAttribute('aria-label', `${t('ui_category_name')} ${i + 1}`); const sync=()=>{this.syncDraft();this.debouncedPreview();}; name.oninput=()=>{this.data.labels[i]=name.value;sync();}; name.onblur=()=>{if(this.data.type==='gauge')this.renderSeriesSettings();}; const numberCell = (value,label,update) => { const input=this.node(row,'input'); input.type='number';input.step='any';input.value=value; input.setAttribute('aria-label',label); input.oninput=()=>{update(input.valueAsNumber);sync();}; }; if (xy) numberCell(this.data.xValues[i],`X ${i+1}`,value=>{this.data.xValues[i]=value;}); this.data.series.forEach((series,index)=>numberCell(series.values[i], `${t('ui_series')} ${index+1} ${t('ui_value')} ${i+1}`,value=>{series.values[i]=value;})); if (perItem) { const cell=this.node(row,'div','ec-color-cell'); const enabled=this.node(cell,'input');enabled.type='checkbox';enabled.checked=!!this.data.colors[i]; enabled.setAttribute('aria-label',`${t('ui_checkbox_color_title')} ${i+1}`); const picker=this.node(cell,'input');picker.type='color'; picker.value=this.data.colors[i]||this.data.themeColor;picker.disabled=!enabled.checked; picker.setAttribute('aria-label',`${t('ui_color')} ${i+1}`); enabled.onchange=()=>{picker.disabled=!enabled.checked;this.data.colors[i]=enabled.checked?picker.value:undefined;this.debouncedPreview();}; picker.oninput=()=>{this.data.colors[i]=picker.value;this.debouncedPreview();}; } const remove=this.button(row,'×',()=>{ this.data.labels.splice(i,1);this.data.xValues.splice(i,1);this.data.colors.splice(i,1); this.data.series.forEach(series=>series.values.splice(i,1)); this.data.gaugeRow=Math.max(0,Math.min(this.data.gaugeRow,this.data.labels.length-1)); sync();this.renderSeriesSettings();this.renderDataEditor(); },'ec-delete'); remove.setAttribute('aria-label',`${t('ui_delete')} ${i+1}`); }); this.button(this.editorContainer,t('ui_add_row'),()=>{ this.data.labels.push(`${t('ui_category_name')} ${this.data.labels.length+1}`); this.data.xValues.push(this.data.labels.length); this.data.series.forEach(series=>series.values.push(0)); this.syncDraft();this.renderSeriesSettings();this.renderDataEditor(); this.editorContainer.querySelector('.ec-table-row:last-child input')?.focus(); this.debouncedPreview(); }); } syncDraft() { this.textDraft=EchartsCore.serializeDataTable(this.data); this.draftDirty=false; } readTextDraft() { if (!this.draftDirty) return true; const parsed=EchartsCore.parseTextToData(this.textDraft,{xy:this.data.type==='scatter'}); if (!parsed) return false; const previous=this.data.series; this.data.series=parsed.series.map((series,i)=>({ ...(previous.find(old=>old.name===series.name)||previous[i]||{type:'line',yAxisIndex:0,color:''}),...series })); this.data.labels=parsed.labels; this.data.xValues=parsed.xValues || parsed.labels.map((_,i)=>this.data.xValues[i]??i+1); this.data.valueSeriesIndex=Math.min(this.data.valueSeriesIndex,this.data.series.length-1); this.data.gaugeRow=Math.max(0,Math.min(this.data.gaugeRow,this.data.labels.length-1)); this.draftDirty=false; this.renderSeriesSettings(); return true; } result() { if (this.data.type === 'custom') { const data={schemaVersion:2,type:'custom',customOptions:this.customOptions}; EchartsCore.validateChartData(data); return data; } if (!this.readTextDraft()) throw new Error(t('ui_invalid_data')); EchartsCore.validateChartData(this.data); return EchartsCore.normalizeChartData(this.data); } debouncedPreview() { clearTimeout(this._previewTimer); this._previewTimer = setTimeout(() => this.renderLivePreview(), 120); } renderLivePreview() { if (this.closed || !this.previewContainer?.isConnected) return; try { const result = this.result(); const lib = window.echarts; if (!lib) throw new Error(t('ui_library_missing')); this.previewContainer.style.backgroundColor = this.previewDark ? '#1e1e1e' : '#ffffff'; if (!this.previewChart) this.previewChart = lib.init( this.previewContainer, this.previewDark ? 'dark' : null, {renderer:'svg'}); const option = JSON.parse(EchartsCore.generateEchartsOptions(result, this.previewDark)); this.previewChart.setOption(option, true); this.previewChart.resize(); this.previewErrorEl.textContent = ''; this.saveBtn.disabled = false; } catch (error) { this.previewErrorEl.textContent = error.message; this.saveBtn.disabled = true; } } async save() { if (this.saving) return; try { const result = this.result(); this.saving = true; this.saveBtn.disabled = true; await this.onSave(result); this.saved = true; this.close(); } catch (error) { this.previewErrorEl.textContent = error.message; } finally { this.saving = false; this.saveBtn.disabled = false; } } onClose() { this.closed = true; clearTimeout(this._previewTimer); this.resizeObserver?.disconnect(); this.previewChart?.dispose(); this.previewChart = null; if (!this.saved) this.onCancel(); } } //ymjr: The Action validates the editor itself, not just leftover helpers on a reused namespace. EchartsEditorModal.dataSchemaVersion = 2; EchartsCore.EchartsEditorModal = EchartsEditorModal; //ymjr: Preserve arbitrary legacy ECharts JSON instead of reverse-engineering only series[0]. EchartsCore.readStoredChart = stored => { if (stored.rawData && (stored.rawData.schemaVersion || 1) <= 2) return EchartsCore.normalizeChartData({...stored.rawData,type:stored.rawData.type || stored.type}); let option; try { option = typeof stored.value === 'string' ? JSON.parse(stored.value) : stored.value; } catch (error) { const parsed = EchartsCore.parseTextToData(stored.value); if (!parsed) throw error; return EchartsCore.normalizeChartData({...parsed,type:stored.type || 'bar'}); } return EchartsCore.normalizeChartData({type:'custom',customOptions:JSON.stringify(option,null,2)}); }; EchartsCore.storedOptions = (stored, isDark = false) => { const data=EchartsCore.readStoredChart(stored); return JSON.parse(EchartsCore.generateEchartsOptions(data,isDark)); }; //ymjr: A save owns its own EA workbench and commits source text deletion in the same scene update. EchartsCore.applyChartData = async (view, result, {elementId,center,sourceTextIds=[]} = {}) => { if (!view?.excalidrawAPI) throw new Error(t('notice_save_failed')); const data=EchartsCore.normalizeChartData(result); const value=EchartsCore.generateEchartsOptions(data,view.excalidrawAPI.getAppState().theme==='dark'); const local=chartPlugin.ea.getAPI(view); try { const live=local.getViewElements(); let target; if (elementId) { const existing=live.find(element=>element.id===elementId&&!element.isDeleted); if (!existing) throw new Error(t('notice_save_failed')); local.copyViewElementsToEAforEditing([existing]); target=local.getElement(elementId); } else { const point=center || local.getViewCenterPosition(); local.style.backgroundColor='transparent'; local.style.strokeColor='transparent'; target=local.getElement(local.addRect(point.x-260,point.y-180,520,360)); } target.customData={...target.customData,echarts:{type:data.type,value,rawData:data}}; if (sourceTextIds.length) { const text=live.filter(element=>sourceTextIds.includes(element.id)&&element.type==='text'&&!element.isDeleted); local.copyViewElementsToEAforEditing(text); for (const element of text) local.getElement(element.id).isDeleted=true; } if (!await local.addElementsToView(false,true,false)) throw new Error(t('notice_save_failed')); new Notice(t('notice_success')); return target.id; } finally { local.destroy(); } }; EchartsCore.openEditorForElement = (element, view) => { if (!element?.customData?.echarts) return; const initial=EchartsCore.readStoredChart(element.customData.echarts); return new EchartsCore.EchartsEditorModal(app,initial,result=> EchartsCore.applyChartData(view,result,{elementId:element.id})).open(); }; const handlePaste = async context => { const view=context?.ea?.targetView; if (!view) return false; // Prefer the original table text: the host's legacy spreadsheet adapter may expose only one value column. let parsed=EchartsCore.parseTextToData(context.payload?.text); if (!parsed && !context.payload?.text && context.spreadsheet?.labels?.length) { const sheet=context.spreadsheet; if (sheet.values?.length===sheet.labels.length&&sheet.values.every(value=> typeof value==='number' ? Number.isFinite(value) : chartNumber(String(value)))) { parsed={labels:sheet.labels,series:[{name:t('ui_series')+' 1',values:sheet.values.map(Number)}]}; } } if (!parsed) return false; const center=context.ea.getViewLastPointerPosition() || context.ea.getViewCenterPosition(); return new Promise(resolve=>{ new EchartsCore.EchartsEditorModal(app,EchartsCore.normalizeChartData(parsed),async result=>{ await EchartsCore.applyChartData(view,result,{center}); resolve(true); },()=>resolve(false)).open(); }); }; //ymjr: Export this element's own options even when no live iframe exists. const handleSvgRender = async context => { const {element,svgRoot,addToRoot,offsetX,offsetY,degree,cx,cy,renderConfig}=context; if (!element?.customData?.echarts) return false; const doc=svgRoot.ownerDocument; const container=doc.createElement('div'); container.style.cssText='position:fixed;left:-100000px;top:0;pointer-events:none;'; doc.body.appendChild(container); let chart; try { const dark=renderConfig?.theme==='dark'; const option=EchartsCore.storedOptions(element.customData.echarts,dark); option.animation=false; chart=window.echarts.init(container,dark?'dark':null,{ renderer:'svg',width:element.width,height:element.height }); chart.setOption(option,true); const parsed=new DOMParser().parseFromString(chart.renderToSVGString(),'image/svg+xml'); const group=doc.createElementNS('http://www.w3.org/2000/svg','g'); group.setAttribute('transform',`translate(${offsetX||0} ${offsetY||0}) rotate(${degree||0} ${cx||0} ${cy||0})`); group.setAttribute('opacity',String((element.opacity??100)/100)); group.appendChild(doc.importNode(parsed.documentElement,true)); addToRoot(group,element); return true; } finally { chart?.dispose(); container.remove(); } }; const setupCustomDocumentRenderer = () => { const echartContent = getEcharts(); const loadEcharts = (doAfterLoaded) => { if (window.echarts) { if (doAfterLoaded) doAfterLoaded(); return; } if (document.getElementById("script-echarts")) return; try { let script = document.createElement("script"); script.id = "script-echarts"; script.onload = doAfterLoaded ? doAfterLoaded : null; script.textContent = echartContent; (document.head || document.body || document.documentElement).appendChild(script); return script; } catch (e) { console.error("[Feature: Echarts] Failed to inject echarts script:", e); } }; // 挂载时立即执行一次预加载,确保全局 window.echarts 就绪 loadEcharts(); const renderWebViewByData = (src, view, id, appState) => { const ReactInstance = getReact(view); if (!ReactInstance) { console.error("[Feature: Echarts] React instance not found"); return null; } return ReactInstance.createElement('iframe', { ref: (ref) => { if (view && typeof view.updateEmbeddableRef === "function") { view.updateEmbeddableRef(id, ref); } }, className: 'excalidraw__embeddable', title: 'Excalidraw Echarts Content', allowFullScreen: true, src: null, style: { overflow: 'hidden', borderRadius: 'var(--embeddable-radius)', border: 'none', width: '100%', height: '100%' }, srcDoc: src, }); }; const renderCustomDocument = function(element, appState) { if (element?.customData?.document) { const view = getActiveExcalidrawView(); return renderWebViewByData(element.customData.document, view, element.id, appState); } if (element?.customData?.echarts) { loadEcharts(); //ymjr: Recompute generated charts for the current canvas theme and escape inline script delimiters. const isDark = appState?.theme ? appState.theme==='dark' : document.body.classList.contains('theme-dark'); const optionStr = JSON.stringify(EchartsCore.storedOptions(element.customData.echarts,isDark)).replace(/
`; const view = getActiveExcalidrawView(); return renderWebViewByData(htmlSrc, view, element.id, appState); } return null; }; EchartsCore.renderCustomDocument = renderCustomDocument; return renderCustomDocument; }; const handleCustomDocumentRender = (context) => { if (!context) return; const { element, appState } = context; const component = EchartsCore.renderCustomDocument?.(element, appState); if (component) { context.component = component; return true; } }; // 画布双击拦截:双击图表直接唤起可视化编辑器原地修改 const handleCanvasDoubleClick = (ctx) => { const { ea, api } = ctx; if (!ea || !api) return false; const { App, sceneX, sceneY } = ctx; if (!App?.scene) return false; const elements = App.scene.getNonDeletedElements(); for (let i = elements.length - 1; i >= 0; i--) { const el = elements[i]; if (!el?.customData?.echarts || el.locked) continue; //ymjr: Hit testing and persistence must use the clicked chart's view, including rotated charts. const angle=el.angle||0; const dx=sceneX-el.x-el.width/2, dy=sceneY-el.y-el.height/2; const x=dx*Math.cos(angle)+dy*Math.sin(angle); const y=-dx*Math.sin(angle)+dy*Math.cos(angle); if (Math.abs(x)<=el.width/2 && Math.abs(y)<=el.height/2) { EchartsCore.openEditorForElement(el,ea.targetView); return true; } } return false; }; async function mountFeature() { if (!window.EA_Core) return console.warn(t("log_no_core")); if (typeof chartPlugin[`disable_${SCRIPT_ID}`] === "function") { chartPlugin[`disable_${SCRIPT_ID}`](); } setupCustomDocumentRenderer(); chartPlugin._ymjr_echarts = EchartsCore; window.EA_Core.registerHook(SCRIPT_ID, 'onPaste', handlePaste, 50); window.EA_Core.registerHook(SCRIPT_ID, 'renderElementToSvg', handleSvgRender, 60); window.EA_Core.registerHook(SCRIPT_ID, 'renderCustomDocument', handleCustomDocumentRender, 50); window.EA_Core.registerHook(SCRIPT_ID, 'handleCanvasDoubleClick', handleCanvasDoubleClick, 80); chartPlugin[`disable_${SCRIPT_ID}`] = () => { if (window.EA_Core) window.EA_Core.unregisterHook(SCRIPT_ID); if (chartPlugin._ymjr_echarts === EchartsCore) delete chartPlugin._ymjr_echarts; console.log(t("log_unmounted", { id: SCRIPT_ID })); }; console.log(t("log_mounted", { id: SCRIPT_ID })); } mountFeature(); function getEcharts() { return '\r\n/*\r\n* Licensed to the Apache Software Foundation (ASF) under one\r\n* or more contributor license agreements. See the NOTICE file\r\n* distributed with this work for additional information\r\n* regarding copyright ownership. The ASF licenses this file\r\n* to you under the Apache License, Version 2.0 (the\r\n* "License"); you may not use this file except in compliance\r\n* with the License. You may obtain a copy of the License at\r\n*\r\n* http://www.apache.org/licenses/LICENSE-2.0\r\n*\r\n* Unless required by applicable law or agreed to in writing,\r\n* software distributed under the License is distributed on an\r\n* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\r\n* KIND, either express or implied. See the License for the\r\n* specific language governing permissions and limitations\r\n* under the License.\r\n*/\r\n\r\n!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).echarts={})}(this,(function(t){"use strict";\r\n/*! *****************************************************************************\r\n Copyright (c) Microsoft Corporation.\r\n\r\n Permission to use, copy, modify, and/or distribute this software for any\r\n purpose with or without fee is hereby granted.\r\n\r\n THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\n REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\n AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\n INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\n LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\n OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\n PERFORMANCE OF THIS SOFTWARE.\r\n ***************************************************************************** */var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n])},e(t,n)};function n(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function i(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(i.prototype=n.prototype,new i)}var i=function(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1},r=new function(){this.browser=new i,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow="undefined"!=typeof window};"object"==typeof wx&&"function"==typeof wx.getSystemInfoSync?(r.wxa=!0,r.touchEventsSupported=!0):"undefined"==typeof document&&"undefined"!=typeof self?r.worker=!0:"undefined"==typeof navigator?(r.node=!0,r.svgSupported=!0):function(t,e){var n=e.browser,i=t.match(/Firefox\\/([\\d.]+)/),r=t.match(/MSIE\\s([\\d.]+)/)||t.match(/Trident\\/.+?rv:(([\\d.]+))/),o=t.match(/Edge?\\/([\\d.]+)/),a=/micromessenger/i.test(t);i&&(n.firefox=!0,n.version=i[1]);r&&(n.ie=!0,n.version=r[1]);o&&(n.edge=!0,n.version=o[1],n.newEdge=+o[1].split(".")[0]>18);a&&(n.weChat=!0);e.svgSupported="undefined"!=typeof SVGRect,e.touchEventsSupported="ontouchstart"in window&&!n.ie&&!n.edge,e.pointerEventsSupported="onpointerdown"in window&&(n.edge||n.ie&&+n.version>=11),e.domSupported="undefined"!=typeof document;var s=document.documentElement.style;e.transform3dSupported=(n.ie&&"transition"in s||n.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in s)&&!("OTransition"in s),e.transformSupported=e.transform3dSupported||n.ie&&+n.version>=9}(navigator.userAgent,r);var o="sans-serif",a="12px "+o;var s,l,u=function(t){var e={};if("undefined"==typeof JSON)return e;for(var n=0;n=a)}}for(var h=this.__startIndex;h t.unconstrainedWidth?null:d:null;i.setStyle("width",f)}var g=i.getBoundingRect();o.width=g.width;var y=(i.style.margin||0)+2.1;o.height=g.height+y,o.y-=(o.height-c)/2}}}function _M(t){return"center"===t.position}function bM(t){var e,n,i=t.getData(),r=[],o=!1,a=(t.get("minShowLabelAngle")||0)*vM,s=i.getLayout("viewRect"),l=i.getLayout("r"),u=s.width,h=s.x,c=s.y,p=s.height;function d(t){t.ignore=!0}i.each((function(t){var s=i.getItemGraphicEl(t),c=s.shape,p=s.getTextContent(),f=s.getTextGuideLine(),g=i.getItemModel(t),y=g.getModel("label"),v=y.get("position")||g.get(["emphasis","label","position"]),m=y.get("distanceToLabelLine"),x=y.get("alignTo"),_=Ur(y.get("edgeDistance"),u),b=y.get("bleedMargin"),w=g.getModel("labelLine"),S=w.get("length");S=Ur(S,u);var M=w.get("length2");if(M=Ur(M,u),Math.abs(c.endAngle-c.startAngle)0?"right":"left":k>0?"left":"right"}var B=Math.PI,F=0,G=y.get("rotate");if(j(G))F=G*(B/180);else if("center"===v)F=0;else if("radial"===G||!0===G){F=k<0?-A+B:-A}else if("tangential"===G&&"outside"!==v&&"outer"!==v){var W=Math.atan2(k,L);W<0&&(W=2*B+W),L>0&&(W=B+W),F=W-B}if(o=!!F,p.x=I,p.y=T,p.rotation=F,p.setStyle({verticalAlign:"middle"}),P){p.setStyle({align:D});var H=p.states.select;H&&(H.x+=p.x,H.y+=p.y)}else{var Y=p.getBoundingRect().clone();Y.applyTransform(p.getComputedTransform());var X=(p.style.margin||0)+2.1;Y.y-=X/2,Y.height+=X,r.push({label:p,labelLine:f,position:v,len:S,len2:M,minTurnAngle:w.get("minTurnAngle"),maxSurfaceAngle:w.get("maxSurfaceAngle"),surfaceNormal:new De(k,L),linePoints:C,textAlign:D,labelDistance:m,labelAlignTo:x,edgeDistance:_,bleedMargin:b,rect:Y,unconstrainedWidth:Y.width,labelStyleWidth:p.style.width})}s.setTextConfig({inside:P})}})),!o&&t.get("avoidLabelOverlap")&&function(t,e,n,i,r,o,a,s){for(var l=[],u=[],h=Number.MAX_VALUE,c=-Number.MAX_VALUE,p=0;p i&&(i=e);var o=i%2?i+2:i+3;r=[];for(var a=0;a5)return;var i=this._model.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]);"none"!==i.behavior&&this._dispatchExpand({axisExpandWindow:i.axisExpandWindow})}this._mouseDownPoint=null},mousemove:function(t){if(!this._mouseDownPoint&&Mk(this,"mousemove")){var e=this._model,n=e.coordinateSystem.getSlidedAxisExpandWindow([t.offsetX,t.offsetY]),i=n.behavior;"jump"===i&&this._throttledDispatchExpand.debounceNextCall(e.get("axisExpandDebounce")),this._throttledDispatchExpand("none"===i?null:{axisExpandWindow:n.axisExpandWindow,animation:"jump"===i?null:{duration:0}})}}};function Mk(t,e){var n=t._model;return n.get("axisExpandable")&&n.get("axisExpandTriggerOn")===e}var Ik=function(t){function e(){var n=null!==t&&t.apply(this,arguments)||this;return n.type=e.type,n}return n(e,t),e.prototype.init=function(){t.prototype.init.apply(this,arguments),this.mergeOption({})},e.prototype.mergeOption=function(t){var e=this.option;t&&C(e,t,!0),this._initDimensions()},e.prototype.contains=function(t,e){var n=t.get("parallelIndex");return null!=n&&e.getComponent("parallel",n)===this},e.prototype.setAxisExpand=function(t){E(["axisExpandable","axisExpandCenter","axisExpandCount","axisExpandWidth","axisExpandWindow"],(function(e){t.hasOwnProperty(e)&&(this.option[e]=t[e])}),this)},e.prototype._initDimensions=function(){var t=this.dimensions=[],e=this.parallelAxisIndex=[];E(B(this.ecModel.queryComponents({mainType:"parallelAxis"}),(function(t){return(t.get("parallelIndex")||0)===this.componentIndex}),this),(function(n){t.push("dim"+n.get("dim")),e.push(n.componentIndex)}))},e.type="parallel",e.dependencies=["parallelAxis"],e.layoutMode="box",e.defaultOption={z:0,left:80,top:60,right:80,bottom:60,layout:"horizontal",axisExpandable:!1,axisExpandCenter:null,axisExpandCount:0,axisExpandWidth:50,axisExpandRate:17,axisExpandDebounce:50,axisExpandSlideTriggerArea:[-.15,.05,.4],axisExpandTriggerOn:"click",parallelAxisDefault:null},e}(Rp),Tk=function(t){function e(e,n,i,r,o){var a=t.call(this,e,n,i)||this;return a.type=r||"value",a.axisIndex=o,a}return n(e,t),e.prototype.isHorizontal=function(){return"horizontal"!==this.coordinateSystem.getModel().get("layout")},e}(nb);function Ck(t,e,n,i,r,o){t=t||0;var a=n[1]-n[0];if(null!=r&&(r=Ak(r,[0,a])),null!=o&&(o=Math.max(o,null!=r?r:0)),"all"===i){var s=Math.abs(e[1]-e[0]);s=Ak(s,[0,a]),r=o=Ak(s,[r,o]),i=0}e[0]=Ak(e[0],n),e[1]=Ak(e[1],n);var l=Dk(e,i);e[i]+=t;var u,h=r||0,c=n.slice();return l.sign<0?c[0]+=h:c[1]-=h,e[i]=Ak(e[i],c),u=Dk(e,i),null!=r&&(u.sign!==l.sign||u.span