chart-generator.js 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  1. // 图表实例,方便后续更新
  2. window.chartInstance = null;
  3. // 注册Chart.js插件
  4. if (Chart && Chart.register) {
  5. // 如果有ChartDataLabels插件,注册它
  6. if (window.ChartDataLabels) {
  7. Chart.register(ChartDataLabels);
  8. }
  9. }
  10. // 生成图表的主函数
  11. function createChart(data, settings) {
  12. // 获取Canvas元素
  13. const canvas = document.getElementById('chart-canvas');
  14. const ctx = canvas.getContext('2d');
  15. // 如果已有图表,先销毁
  16. if (window.chartInstance) {
  17. window.chartInstance.destroy();
  18. }
  19. // 应用颜色方案
  20. applyColorScheme(data, settings.colorScheme, settings.type);
  21. // 配置图表选项
  22. const options = getChartOptions(settings);
  23. // 处理特殊图表类型
  24. let type = settings.type;
  25. let chartData = {...data};
  26. // 检查是否为首系列图表类型
  27. const isFirstSeriesOnly = settings.type.includes(" (首系列)");
  28. if (isFirstSeriesOnly) {
  29. // 提取真正的图表类型
  30. type = settings.type.replace(" (首系列)", "");
  31. // 只保留第一个数据系列
  32. if (chartData.datasets.length > 1) {
  33. const firstDataset = chartData.datasets[0];
  34. chartData.datasets = [{
  35. ...firstDataset,
  36. label: firstDataset.label || '数据'
  37. }];
  38. }
  39. }
  40. // 移除可能存在的旧堆叠设置
  41. if (options.scales && options.scales.x) {
  42. delete options.scales.x.stacked;
  43. }
  44. if (options.scales && options.scales.y) {
  45. delete options.scales.y.stacked;
  46. }
  47. // 移除旧的填充设置和其他特殊属性
  48. chartData.datasets.forEach(dataset => {
  49. delete dataset.fill;
  50. delete dataset.tension;
  51. delete dataset.stepped;
  52. delete dataset.borderDash;
  53. });
  54. // 基本图表类型处理
  55. switch(type) {
  56. // 柱状图系列
  57. case 'horizontalBar':
  58. type = 'bar';
  59. options.indexAxis = 'y';
  60. break;
  61. case 'stackedBar':
  62. type = 'bar';
  63. if (!options.scales) options.scales = {};
  64. if (!options.scales.x) options.scales.x = {};
  65. if (!options.scales.y) options.scales.y = {};
  66. options.scales.x.stacked = true;
  67. options.scales.y.stacked = true;
  68. break;
  69. case 'groupedBar':
  70. type = 'bar';
  71. // 分组柱状图是默认行为
  72. break;
  73. case 'gradientBar':
  74. type = 'bar';
  75. // 渐变效果在applyColorScheme函数中处理
  76. break;
  77. case 'barWithError':
  78. type = 'bar';
  79. // 添加误差线
  80. chartData.datasets.forEach(dataset => {
  81. dataset.errorBars = {
  82. y: {
  83. plus: dataset.data.map(() => Math.random() * 5 + 2),
  84. minus: dataset.data.map(() => Math.random() * 5 + 2)
  85. }
  86. };
  87. });
  88. break;
  89. case 'rangeBar':
  90. type = 'bar';
  91. // 转换数据为范围格式
  92. chartData.datasets.forEach(dataset => {
  93. dataset.data = dataset.data.map(value => {
  94. const min = Math.max(0, value - Math.random() * value * 0.4);
  95. return [min, value];
  96. });
  97. });
  98. break;
  99. // 线/面积图系列
  100. case 'area':
  101. type = 'line';
  102. chartData.datasets.forEach(dataset => {
  103. dataset.fill = true;
  104. });
  105. break;
  106. case 'curvedLine':
  107. type = 'line';
  108. chartData.datasets.forEach(dataset => {
  109. dataset.tension = 0.4; // 更平滑的曲线
  110. });
  111. break;
  112. case 'stepLine':
  113. type = 'line';
  114. chartData.datasets.forEach(dataset => {
  115. dataset.stepped = true;
  116. });
  117. break;
  118. case 'stackedArea':
  119. type = 'line';
  120. chartData.datasets.forEach(dataset => {
  121. dataset.fill = true;
  122. });
  123. if (!options.scales) options.scales = {};
  124. if (!options.scales.y) options.scales.y = {};
  125. options.scales.y.stacked = true;
  126. break;
  127. case 'streamgraph':
  128. type = 'line';
  129. // 流图效果:堆叠面积图 + 居中对齐
  130. chartData.datasets.forEach(dataset => {
  131. dataset.fill = true;
  132. });
  133. if (!options.scales) options.scales = {};
  134. if (!options.scales.y) options.scales.y = {};
  135. options.scales.y.stacked = true;
  136. options.scales.y.offset = true; // 居中对齐堆叠
  137. break;
  138. case 'timeline':
  139. type = 'line';
  140. chartData.datasets.forEach(dataset => {
  141. dataset.stepped = 'before';
  142. dataset.borderDash = [5, 5]; // 虚线效果
  143. });
  144. break;
  145. // 饼图/环形图系列
  146. case 'halfPie':
  147. type = 'doughnut';
  148. options.circumference = Math.PI;
  149. options.rotation = -Math.PI / 2;
  150. break;
  151. case 'nestedPie':
  152. type = 'doughnut';
  153. // 嵌套效果通过多个饼图叠加实现,简化实现仅调整内外半径
  154. if (chartData.datasets.length > 0) {
  155. chartData.datasets[0].radius = '70%';
  156. chartData.datasets[0].weight = 0.7;
  157. }
  158. break;
  159. // 散点/气泡图系列
  160. case 'scatter':
  161. chartData.datasets = transformScatterData(chartData.datasets);
  162. break;
  163. case 'bubble':
  164. chartData.datasets = transformBubbleData(chartData.datasets);
  165. type = 'bubble';
  166. break;
  167. case 'scatterSmooth':
  168. chartData.datasets = transformScatterData(chartData.datasets);
  169. type = 'scatter';
  170. // 添加趋势线
  171. chartData.datasets.forEach(dataset => {
  172. const smoothedDataset = {
  173. ...dataset,
  174. type: 'line',
  175. data: [...dataset.data],
  176. pointRadius: 0,
  177. tension: 0.4,
  178. fill: false
  179. };
  180. chartData.datasets.push(smoothedDataset);
  181. });
  182. break;
  183. // 专业图表系列
  184. case 'funnel':
  185. type = 'bar';
  186. // 简化的漏斗图实现
  187. options.indexAxis = 'y';
  188. if (chartData.datasets.length > 0) {
  189. // 对数据进行排序
  190. const sortedData = [...chartData.datasets[0].data].sort((a, b) => b - a);
  191. chartData.datasets[0].data = sortedData;
  192. // 确保Y轴反转
  193. if (!options.scales) options.scales = {};
  194. if (!options.scales.y) options.scales.y = {};
  195. options.scales.y.reverse = true;
  196. }
  197. break;
  198. case 'gauge':
  199. type = 'doughnut';
  200. // 简化的仪表盘实现
  201. if (chartData.datasets.length > 0 && chartData.datasets[0].data.length > 0) {
  202. const value = chartData.datasets[0].data[0];
  203. const max = Math.max(...chartData.datasets[0].data) * 1.2;
  204. const remainder = max - value;
  205. chartData.datasets[0].data = [value, remainder];
  206. chartData.datasets[0].backgroundColor = ['#36A2EB', '#E0E0E0'];
  207. chartData.labels = ['Value', ''];
  208. options.circumference = Math.PI;
  209. options.rotation = -Math.PI;
  210. options.cutout = '70%';
  211. }
  212. break;
  213. case 'boxplot':
  214. // 简化的箱线图实现(基于柱状图)
  215. type = 'bar';
  216. // 转换数据为箱线图格式
  217. chartData.datasets.forEach(dataset => {
  218. dataset.data = dataset.data.map(value => {
  219. const q1 = Math.max(0, value * 0.7);
  220. const median = value * 0.85;
  221. const q3 = value * 1.15;
  222. const min = Math.max(0, q1 - (median - q1));
  223. const max = q3 + (q3 - median);
  224. return [min, q1, median, q3, max];
  225. });
  226. });
  227. break;
  228. case 'waterfall':
  229. type = 'bar';
  230. // 瀑布图实现
  231. if (chartData.datasets.length > 0) {
  232. const data = chartData.datasets[0].data;
  233. let cumulative = 0;
  234. // 创建新的数据数组,包含每个点的起点和终点
  235. const waterfallData = data.map((value, index) => {
  236. const start = cumulative;
  237. cumulative += value;
  238. return {
  239. start: start,
  240. end: cumulative,
  241. value: value
  242. };
  243. });
  244. // 转换为柱状图数据
  245. chartData.datasets[0].data = waterfallData.map(d => d.end - d.start);
  246. // 添加起点数据集
  247. chartData.datasets.push({
  248. label: '起点',
  249. data: waterfallData.map(d => d.start),
  250. backgroundColor: 'rgba(0,0,0,0)',
  251. borderColor: 'rgba(0,0,0,0)',
  252. stack: 'waterfall'
  253. });
  254. // 设置为堆叠柱状图
  255. if (!options.scales) options.scales = {};
  256. if (!options.scales.x) options.scales.x = {};
  257. if (!options.scales.y) options.scales.y = {};
  258. options.scales.x.stacked = true;
  259. options.scales.y.stacked = true;
  260. }
  261. break;
  262. case 'treemap':
  263. case 'sunburst':
  264. case 'sankey':
  265. case 'chord':
  266. case 'network':
  267. // 这些高级图表需要专门的库支持,这里简化为提示信息
  268. type = 'bar';
  269. if (chartData.datasets.length > 0) {
  270. // 显示一个提示信息
  271. chartData.datasets = [{
  272. label: `${settings.type}需要专门的图表库支持`,
  273. data: [100],
  274. backgroundColor: '#f8d7da'
  275. }];
  276. chartData.labels = ['请尝试其他图表类型'];
  277. }
  278. break;
  279. }
  280. // 热力图特殊处理
  281. if (type === 'heatmap') {
  282. // 热力图不是Chart.js的标准类型,需要使用插件或自定义渲染
  283. // 简单实现一个基于颜色渐变的矩阵图
  284. type = 'matrix';
  285. renderHeatmap(ctx, chartData, options);
  286. return;
  287. }
  288. // 饼图、环形图和极地面积图特殊处理 (如果不是由其他类型转换而来的)
  289. if (['pie', 'doughnut', 'polarArea'].includes(type) && !['halfPie', 'nestedPie', 'gauge'].includes(settings.type) && !isFirstSeriesOnly) {
  290. // 如果有多个数据集,只取第一个
  291. if (chartData.datasets.length > 1) {
  292. const firstDataset = chartData.datasets[0];
  293. chartData.datasets = [{
  294. ...firstDataset,
  295. label: undefined // 这些图表类型不需要数据集标签
  296. }];
  297. }
  298. }
  299. // 创建图表实例
  300. window.chartInstance = new Chart(ctx, {
  301. type: type,
  302. data: chartData,
  303. options: options
  304. });
  305. // 设置标记属性,表示图表已渲染,去除背景
  306. canvas.setAttribute('data-chart-rendered', 'true');
  307. return window.chartInstance;
  308. }
  309. // 辅助函数:获取当前图表实例
  310. function getChartInstance() {
  311. return window.chartInstance;
  312. }
  313. // 辅助函数:设置当前图表实例
  314. function setChartInstance(instance) {
  315. window.chartInstance = instance;
  316. }
  317. // 获取图表配置选项
  318. function getChartOptions(settings) {
  319. const options = {
  320. responsive: true,
  321. maintainAspectRatio: false,
  322. plugins: {
  323. title: {
  324. display: !!settings.title,
  325. text: settings.title,
  326. font: {
  327. size: 18,
  328. weight: 'bold'
  329. },
  330. padding: {
  331. top: 10,
  332. bottom: 20
  333. }
  334. },
  335. legend: {
  336. display: settings.legendPosition !== 'none',
  337. position: settings.legendPosition === 'none' ? 'top' : settings.legendPosition,
  338. labels: {
  339. usePointStyle: true,
  340. padding: 15,
  341. font: {
  342. size: 12
  343. }
  344. }
  345. },
  346. tooltip: {
  347. enabled: true,
  348. backgroundColor: 'rgba(0, 0, 0, 0.7)',
  349. titleFont: {
  350. size: 14
  351. },
  352. bodyFont: {
  353. size: 13
  354. },
  355. padding: 10,
  356. displayColors: true
  357. }
  358. },
  359. animation: {
  360. duration: settings.animateChart ? 1000 : 0,
  361. easing: 'easeOutQuart'
  362. }
  363. };
  364. // 检查是否为简单数据模式(只有一个数据集)
  365. const isSimpleData = settings.isSimpleData ||
  366. (window.chartData && window.chartData.datasets && window.chartData.datasets.length === 1);
  367. // 如果是简单数据模式,隐藏图例
  368. if (isSimpleData) {
  369. options.plugins.legend.display = false;
  370. }
  371. // 只有部分图表类型需要轴线配置
  372. if (!['pie', 'doughnut', 'polarArea'].includes(settings.type.replace(" (首系列)", ""))) { // 兼容(首系列)后缀
  373. options.scales = {
  374. x: {
  375. title: {
  376. display: !!settings.xAxisLabel,
  377. text: settings.xAxisLabel,
  378. font: {
  379. size: 14,
  380. weight: 'bold'
  381. },
  382. padding: {
  383. top: 10
  384. }
  385. },
  386. grid: {
  387. display: settings.showGridLines,
  388. color: 'rgba(0, 0, 0, 0.1)'
  389. },
  390. ticks: {
  391. font: {
  392. size: 12
  393. }
  394. }
  395. },
  396. y: {
  397. title: {
  398. display: !!settings.yAxisLabel,
  399. text: settings.yAxisLabel,
  400. font: {
  401. size: 14,
  402. weight: 'bold'
  403. },
  404. padding: {
  405. bottom: 10
  406. }
  407. },
  408. grid: {
  409. display: settings.showGridLines,
  410. color: 'rgba(0, 0, 0, 0.1)'
  411. },
  412. ticks: {
  413. font: {
  414. size: 12
  415. },
  416. beginAtZero: true
  417. }
  418. }
  419. };
  420. // 水平柱状图X和Y轴配置需要互换
  421. if (settings.type === 'horizontalBar') {
  422. const temp = options.scales.x;
  423. options.scales.x = options.scales.y;
  424. options.scales.y = temp;
  425. }
  426. }
  427. // 数据标签配置
  428. if (settings.showDataLabels) {
  429. options.plugins.datalabels = {
  430. display: true,
  431. color: function(context) {
  432. const actualType = settings.type.replace(" (首系列)", "");
  433. const dataset = context.dataset;
  434. // 首先检查数据集是否有自定义的datalabels配置
  435. if (dataset.datalabels && dataset.datalabels.color) {
  436. const labelColors = dataset.datalabels.color;
  437. // 如果color是数组,则使用对应索引的颜色
  438. if (Array.isArray(labelColors)) {
  439. return labelColors[context.dataIndex] || '#333333';
  440. }
  441. // 如果color是单个颜色值
  442. return labelColors;
  443. }
  444. // 如果没有自定义配置,则使用智能检测
  445. // 为饼图和环形图使用对比色
  446. if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
  447. // 获取背景色
  448. const index = context.dataIndex;
  449. const backgroundColor = dataset.backgroundColor[index];
  450. // 计算背景色的亮度
  451. return isColorDark(backgroundColor) ? '#ffffff' : '#000000';
  452. } else if (actualType === 'bar' || actualType === 'horizontalBar' ||
  453. actualType === 'stackedBar' || actualType === 'gradientBar') {
  454. // 柱状图系列也需要对比色
  455. let backgroundColor;
  456. // 背景色可能是数组或单个颜色
  457. if (Array.isArray(dataset.backgroundColor)) {
  458. backgroundColor = dataset.backgroundColor[context.dataIndex];
  459. } else {
  460. backgroundColor = dataset.backgroundColor;
  461. }
  462. return isColorDark(backgroundColor) ? '#ffffff' : '#333333';
  463. } else {
  464. // 其他图表类型使用默认深色
  465. return '#333333';
  466. }
  467. },
  468. align: function(context) {
  469. const dataset = context.dataset;
  470. // 使用数据集中的align配置(如果有的话)
  471. if (dataset.datalabels && dataset.datalabels.align) {
  472. return dataset.datalabels.align;
  473. }
  474. // 默认配置
  475. const chartType = settings.type.replace(" (首系列)", "");
  476. if (['line', 'area', 'scatter', 'bubble'].includes(chartType)) {
  477. return 'top';
  478. }
  479. return 'center';
  480. },
  481. font: {
  482. weight: 'bold'
  483. },
  484. formatter: function(value, context) {
  485. const actualType = settings.type.replace(" (首系列)", "");
  486. // 饼图、环形图和极地面积图显示百分比
  487. if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
  488. // 计算百分比
  489. const dataset = context.chart.data.datasets[context.datasetIndex];
  490. const total = dataset.data.reduce((total, value) => total + value, 0);
  491. const percentage = ((value / total) * 100).toFixed(1) + '%';
  492. // 对于较小的扇区只显示百分比,否则显示值和百分比
  493. const percent = value / total * 100;
  494. if (percent < 5) {
  495. return percentage;
  496. } else {
  497. return `${value} (${percentage})`;
  498. }
  499. }
  500. // 对散点图特殊处理
  501. if (settings.type === 'scatter') {
  502. if (context && context.dataset && context.dataset.data &&
  503. context.dataset.data[context.dataIndex] &&
  504. typeof context.dataset.data[context.dataIndex].y !== 'undefined') {
  505. return context.dataset.data[context.dataIndex].y;
  506. }
  507. return '';
  508. }
  509. return value;
  510. }
  511. };
  512. }
  513. return options;
  514. }
  515. // 判断颜色是否为深色
  516. function isColorDark(color) {
  517. // 处理rgba格式
  518. if (color && color.startsWith('rgba')) {
  519. const parts = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
  520. if (parts) {
  521. const r = parseInt(parts[1]);
  522. const g = parseInt(parts[2]);
  523. const b = parseInt(parts[3]);
  524. // 计算亮度 (根据人眼对RGB的敏感度加权)
  525. const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
  526. return brightness < 0.7; // 亮度小于0.7认为是深色
  527. }
  528. }
  529. // 处理rgb格式
  530. if (color && color.startsWith('rgb(')) {
  531. const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  532. if (parts) {
  533. const r = parseInt(parts[1]);
  534. const g = parseInt(parts[2]);
  535. const b = parseInt(parts[3]);
  536. const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
  537. return brightness < 0.7;
  538. }
  539. }
  540. // 处理十六进制格式
  541. if (color && color.startsWith('#')) {
  542. color = color.replace('#', '');
  543. const r = parseInt(color.length === 3 ? color.substring(0, 1).repeat(2) : color.substring(0, 2), 16);
  544. const g = parseInt(color.length === 3 ? color.substring(1, 2).repeat(2) : color.substring(2, 4), 16);
  545. const b = parseInt(color.length === 3 ? color.substring(2, 3).repeat(2) : color.substring(4, 6), 16);
  546. const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
  547. return brightness < 0.7;
  548. }
  549. // 默认返回true,使用白色文本
  550. return true;
  551. }
  552. // 应用颜色方案
  553. function applyColorScheme(data, colorScheme, chartType) {
  554. // 定义颜色方案 - 全新设计,确保各个方案风格迥异
  555. const colorSchemes = {
  556. default: [
  557. '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
  558. '#6f42c1', '#fd7e14', '#20c9a6', '#36b9cc', '#858796'
  559. ],
  560. pastel: [
  561. '#FFB6C1', '#FFD700', '#98FB98', '#87CEFA', '#FFA07A',
  562. '#DDA0DD', '#FFDAB9', '#B0E0E6', '#F0E68C', '#E6E6FA'
  563. ],
  564. bright: [
  565. '#FF1E1E', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF',
  566. '#FF00FF', '#FF7F00', '#FF1493', '#00FA9A', '#7B68EE'
  567. ],
  568. cool: [
  569. '#5F4B8B', '#42BFDD', '#00A7E1', '#00344B', '#143642',
  570. '#0F8B8D', '#4CB5F5', '#1D3557', '#A8DADC', '#457B9D'
  571. ],
  572. warm: [
  573. '#FF7700', '#FF9E00', '#FFCF00', '#FFF400', '#E20000',
  574. '#D91A1A', '#A60000', '#FF5252', '#FF7B7B', '#FFBF69'
  575. ],
  576. corporate: [
  577. '#003F5C', '#2F4B7C', '#665191', '#A05195', '#D45087',
  578. '#F95D6A', '#FF7C43', '#FFA600', '#004D40', '#00695C'
  579. ],
  580. contrast: [
  581. '#000000', '#E63946', '#457B9D', '#F1C40F', '#2ECC71',
  582. '#9B59B6', '#1ABC9C', '#F39C12', '#D35400', '#7F8C8D'
  583. ],
  584. rainbow: [
  585. '#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF',
  586. '#4B0082', '#9400D3', '#FF1493', '#00FFFF', '#FF00FF'
  587. ],
  588. earth: [
  589. '#5D4037', '#795548', '#A1887F', '#4E342E', '#3E2723',
  590. '#33691E', '#558B2F', '#7CB342', '#8D6E63', '#6D4C41'
  591. ],
  592. ocean: [
  593. '#006064', '#00838F', '#0097A7', '#00ACC1', '#00BCD4',
  594. '#26C6DA', '#4DD0E1', '#80DEEA', '#01579B', '#0277BD'
  595. ],
  596. vintage: [
  597. '#8D8741', '#659DBD', '#DAAD86', '#BC986A', '#FBEEC1',
  598. '#605B56', '#837A75', '#9E8B8B', '#D8C3A5', '#E8DDCD'
  599. ]
  600. };
  601. // 获取选定的颜色方案
  602. const colors = colorSchemes[colorScheme] || colorSchemes.default;
  603. const actualChartType = chartType.replace(" (首系列)", ""); // 获取基础类型
  604. // 为每个数据集应用颜色
  605. data.datasets.forEach((dataset, index) => {
  606. const color = colors[index % colors.length];
  607. // 设置不同图表类型的颜色
  608. if (['pie', 'doughnut', 'polarArea', 'halfPie', 'nestedPie', 'gauge'].includes(actualChartType)) {
  609. // 这些图表类型需要为每个数据点设置不同颜色
  610. // 对于gauge特殊处理,不使用这种方式
  611. if (actualChartType === 'gauge' && dataset.backgroundColor) {
  612. // 保留gauge的特殊颜色设置
  613. } else {
  614. dataset.backgroundColor = dataset.data.map((_, i) => colors[i % colors.length]);
  615. dataset.borderColor = 'white';
  616. dataset.borderWidth = 1;
  617. // 为每个扇区添加对应的前景色(用于数据标签)
  618. dataset.datalabels = {
  619. color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#000000')
  620. };
  621. }
  622. } else if (['line', 'area', 'stackedArea', 'curvedLine', 'stepLine', 'timeline', 'streamgraph'].includes(actualChartType)) {
  623. // 折线图和面积图样式
  624. dataset.borderColor = color;
  625. // 根据图表类型调整透明度
  626. let alpha = 0.1; // 默认折线图半透明
  627. if (['area', 'stackedArea', 'streamgraph'].includes(actualChartType)) {
  628. alpha = 0.3; // 面积图相对更不透明
  629. }
  630. dataset.backgroundColor = hexToRgba(color, alpha);
  631. dataset.pointBackgroundColor = color;
  632. dataset.pointBorderColor = '#fff';
  633. dataset.pointHoverBackgroundColor = '#fff';
  634. dataset.pointHoverBorderColor = color;
  635. // 特殊线型
  636. if (actualChartType === 'curvedLine') {
  637. dataset.tension = 0.4;
  638. } else if (actualChartType === 'stepLine') {
  639. dataset.stepped = true;
  640. } else if (actualChartType === 'timeline') {
  641. dataset.stepped = 'before';
  642. dataset.borderDash = [5, 5];
  643. } else {
  644. dataset.tension = 0.3;
  645. }
  646. // 设置数据标签颜色
  647. // 对于线图和面积图,标签通常放在点的上方,使用与线条相同的颜色
  648. dataset.datalabels = {
  649. color: isColorDark(color) ? color : '#333333',
  650. align: 'top'
  651. };
  652. } else if (actualChartType === 'radar') {
  653. // 雷达图样式
  654. dataset.borderColor = color;
  655. dataset.backgroundColor = hexToRgba(color, 0.2);
  656. dataset.pointBackgroundColor = color;
  657. dataset.pointBorderColor = '#fff';
  658. // 雷达图数据标签颜色 - 使用与边框相同的颜色
  659. dataset.datalabels = {
  660. color: isColorDark(color) ? color : '#333333'
  661. };
  662. } else if (['scatter', 'bubble', 'scatterSmooth'].includes(actualChartType)) {
  663. // 散点图样式
  664. dataset.backgroundColor = color;
  665. dataset.borderColor = hexToRgba(color, 0.8);
  666. // 散点平滑图特殊处理
  667. if (actualChartType === 'scatterSmooth' && dataset.type === 'line') {
  668. dataset.borderColor = color;
  669. dataset.backgroundColor = 'transparent';
  670. }
  671. // 散点图数据标签颜色
  672. dataset.datalabels = {
  673. color: isColorDark(color) ? '#ffffff' : '#333333',
  674. align: 'top'
  675. };
  676. } else if (actualChartType === 'gradientBar') {
  677. // 渐变柱状图
  678. const ctx = document.createElement('canvas').getContext('2d');
  679. const gradient = ctx.createLinearGradient(0, 0, 0, 300);
  680. gradient.addColorStop(0, color);
  681. gradient.addColorStop(1, hexToRgba(color, 0.3));
  682. dataset.backgroundColor = gradient;
  683. dataset.borderColor = color;
  684. dataset.borderWidth = 1;
  685. dataset.hoverBackgroundColor = color;
  686. // 渐变柱状图标签颜色 - 使用顶部颜色判断
  687. dataset.datalabels = {
  688. color: isColorDark(color) ? '#ffffff' : '#333333'
  689. };
  690. } else if (actualChartType === 'waterfall') {
  691. // 瀑布图特殊处理
  692. if (dataset.label === '起点') {
  693. // 这是为瀑布图添加的起点数据集,保持透明
  694. } else {
  695. const values = dataset.data;
  696. // 根据值的正负设置不同颜色
  697. const positiveColor = '#36b9cc';
  698. const negativeColor = '#e74a3b';
  699. dataset.backgroundColor = values.map(value =>
  700. value >= 0 ? hexToRgba(positiveColor, 0.7) : hexToRgba(negativeColor, 0.7)
  701. );
  702. dataset.borderColor = values.map(value =>
  703. value >= 0 ? positiveColor : negativeColor
  704. );
  705. dataset.borderWidth = 1;
  706. // 瀑布图数据标签颜色 - 根据每个柱子的背景色决定
  707. dataset.datalabels = {
  708. color: values.map(value =>
  709. value >= 0 ? (isColorDark(positiveColor) ? '#ffffff' : '#333333') :
  710. (isColorDark(negativeColor) ? '#ffffff' : '#333333')
  711. )
  712. };
  713. }
  714. } else if (actualChartType === 'funnel') {
  715. // 漏斗图特殊处理 - 使用渐变颜色
  716. const data = dataset.data;
  717. if (data.length) {
  718. dataset.backgroundColor = data.map((_, i) => {
  719. const ratio = 1 - (i / data.length); // 1 到 0
  720. return hexToRgba(color, 0.5 + ratio * 0.5); // 透明度从1到0.5
  721. });
  722. dataset.borderColor = color;
  723. dataset.borderWidth = 1;
  724. // 漏斗图数据标签颜色 - 根据每个部分的背景色决定
  725. dataset.datalabels = {
  726. color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#333333')
  727. };
  728. }
  729. } else {
  730. // 默认样式(用于柱状图等)
  731. dataset.backgroundColor = hexToRgba(color, 0.7);
  732. dataset.borderColor = color;
  733. dataset.borderWidth = 1;
  734. dataset.hoverBackgroundColor = color;
  735. // 默认数据标签颜色 - 根据背景色决定
  736. dataset.datalabels = {
  737. color: isColorDark(dataset.backgroundColor) ? '#ffffff' : '#333333'
  738. };
  739. }
  740. });
  741. }
  742. // 将十六进制颜色转换为rgba格式
  743. function hexToRgba(hex, alpha) {
  744. // 移除井号
  745. hex = hex.replace('#', '');
  746. // 解析RGB值
  747. const r = parseInt(hex.length === 3 ? hex.substring(0, 1).repeat(2) : hex.substring(0, 2), 16);
  748. const g = parseInt(hex.length === 3 ? hex.substring(1, 2).repeat(2) : hex.substring(2, 4), 16);
  749. const b = parseInt(hex.length === 3 ? hex.substring(2, 3).repeat(2) : hex.substring(4, 6), 16);
  750. // 返回rgba字符串
  751. return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  752. }
  753. // 渲染热力图(自定义实现)
  754. function renderHeatmap(ctx, data, options) {
  755. // 清除Canvas
  756. ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  757. // 设置尺寸和边距
  758. const margin = {
  759. top: 50,
  760. right: 30,
  761. bottom: 50,
  762. left: 60
  763. };
  764. const width = ctx.canvas.width - margin.left - margin.right;
  765. const height = ctx.canvas.height - margin.top - margin.bottom;
  766. // 获取数据
  767. const rows = data.labels;
  768. const columns = data.datasets.map(dataset => dataset.label);
  769. // 创建值矩阵
  770. const matrix = [];
  771. rows.forEach((_, rowIndex) => {
  772. const row = [];
  773. data.datasets.forEach(dataset => {
  774. row.push(dataset.data[rowIndex]);
  775. });
  776. matrix.push(row);
  777. });
  778. // 找出最大值和最小值
  779. const allValues = matrix.flat();
  780. const min = Math.min(...allValues);
  781. const max = Math.max(...allValues);
  782. // 绘制标题
  783. if (options.plugins && options.plugins.title && options.plugins.title.display) {
  784. ctx.textAlign = 'center';
  785. ctx.font = '18px Arial';
  786. ctx.fillStyle = '#333';
  787. ctx.fillText(options.plugins.title.text, ctx.canvas.width / 2, 25);
  788. }
  789. // 绘制单元格和标签
  790. const cellWidth = width / columns.length;
  791. const cellHeight = height / rows.length;
  792. // 行标签(Y轴)
  793. ctx.textAlign = 'right';
  794. ctx.textBaseline = 'middle';
  795. ctx.font = '12px Arial';
  796. ctx.fillStyle = '#666';
  797. rows.forEach((label, i) => {
  798. const y = margin.top + i * cellHeight + cellHeight / 2;
  799. ctx.fillText(label, margin.left - 10, y);
  800. });
  801. // 列标签(X轴)
  802. ctx.textAlign = 'center';
  803. ctx.textBaseline = 'top';
  804. columns.forEach((label, i) => {
  805. const x = margin.left + i * cellWidth + cellWidth / 2;
  806. ctx.fillText(label, x, margin.top + height + 10);
  807. });
  808. // 绘制热力图单元格
  809. matrix.forEach((row, i) => {
  810. row.forEach((value, j) => {
  811. // 归一化值 (0-1)
  812. const normalizedValue = (value - min) / (max - min || 1);
  813. // 计算颜色(红-黄-绿渐变)
  814. const color = getHeatmapColor(normalizedValue);
  815. // 绘制单元格
  816. const x = margin.left + j * cellWidth;
  817. const y = margin.top + i * cellHeight;
  818. ctx.fillStyle = color;
  819. ctx.fillRect(x, y, cellWidth, cellHeight);
  820. // 添加值标签,根据背景色的亮度自动选择标签颜色
  821. const brightness = getColorBrightness(color);
  822. ctx.fillStyle = brightness < 0.7 ? 'white' : 'black'; // 亮度阈值为0.7
  823. ctx.textAlign = 'center';
  824. ctx.textBaseline = 'middle';
  825. ctx.font = 'bold 12px Arial';
  826. ctx.fillText(value, x + cellWidth / 2, y + cellHeight / 2);
  827. });
  828. });
  829. // 绘制坐标轴
  830. ctx.strokeStyle = '#ddd';
  831. ctx.lineWidth = 1;
  832. // X轴
  833. ctx.beginPath();
  834. ctx.moveTo(margin.left, margin.top + height);
  835. ctx.lineTo(margin.left + width, margin.top + height);
  836. ctx.stroke();
  837. // Y轴
  838. ctx.beginPath();
  839. ctx.moveTo(margin.left, margin.top);
  840. ctx.lineTo(margin.left, margin.top + height);
  841. ctx.stroke();
  842. }
  843. // 获取颜色亮度
  844. function getColorBrightness(color) {
  845. // 处理rgb格式
  846. if (color.startsWith('rgb(')) {
  847. const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  848. if (parts) {
  849. const r = parseInt(parts[1]);
  850. const g = parseInt(parts[2]);
  851. const b = parseInt(parts[3]);
  852. // 计算亮度 (根据人眼对RGB的敏感度加权)
  853. return (r * 0.299 + g * 0.587 + b * 0.114) / 255;
  854. }
  855. }
  856. // 默认返回0.5
  857. return 0.5;
  858. }
  859. // 获取热力图颜色
  860. function getHeatmapColor(value) {
  861. // 红-黄-绿渐变
  862. const r = value < 0.5 ? 255 : Math.round(255 * (1 - 2 * (value - 0.5)));
  863. const g = value < 0.5 ? Math.round(255 * (2 * value)) : 255;
  864. const b = 0;
  865. return `rgb(${r}, ${g}, ${b})`;
  866. }
  867. // 注册Chart.js插件以支持数据标签
  868. Chart.register({
  869. id: 'datalabels',
  870. beforeDraw: (chart) => {
  871. const ctx = chart.ctx;
  872. const options = chart.options.plugins.datalabels;
  873. if (!options || !options.display) {
  874. return;
  875. }
  876. chart.data.datasets.forEach((dataset, datasetIndex) => {
  877. const meta = chart.getDatasetMeta(datasetIndex);
  878. meta.data.forEach((element, index) => {
  879. // 获取值
  880. let value = dataset.data[index];
  881. if (typeof value === 'object' && value !== null) {
  882. // 散点图等复杂数据结构
  883. value = value.y;
  884. }
  885. // 获取位置
  886. const { x, y } = element.getCenterPoint();
  887. // 确定文本颜色
  888. let fillColor;
  889. if (typeof options.color === 'function') {
  890. fillColor = options.color({
  891. datasetIndex,
  892. index,
  893. dataset,
  894. dataIndex: index,
  895. chart: chart
  896. });
  897. } else {
  898. fillColor = options.color || '#666';
  899. }
  900. ctx.fillStyle = fillColor;
  901. // 设置字体
  902. ctx.font = options.font.weight + ' 12px Arial';
  903. ctx.textAlign = 'center';
  904. ctx.textBaseline = 'middle';
  905. // 格式化值
  906. let text = typeof options.formatter === 'function'
  907. ? options.formatter(value, {
  908. datasetIndex,
  909. index,
  910. dataset,
  911. dataIndex: index,
  912. chart: chart
  913. })
  914. : value;
  915. // 绘制文本
  916. ctx.fillText(text, x, y - 15);
  917. });
  918. });
  919. }
  920. });
  921. // 辅助函数:转换散点图数据
  922. function transformScatterData(datasets) {
  923. return datasets.map(dataset => {
  924. if (!dataset.data || !Array.isArray(dataset.data)) {
  925. return {
  926. ...dataset,
  927. data: []
  928. };
  929. }
  930. return {
  931. ...dataset,
  932. data: dataset.data.map((value, index) => {
  933. // 确保value是一个有效的数值
  934. const y = parseFloat(value);
  935. if (isNaN(y)) {
  936. return { x: index + 1, y: 0 };
  937. }
  938. return { x: index + 1, y: y };
  939. })
  940. };
  941. });
  942. }
  943. // 辅助函数:转换气泡图数据
  944. function transformBubbleData(datasets) {
  945. return datasets.map(dataset => {
  946. if (!dataset.data || !Array.isArray(dataset.data)) {
  947. return {
  948. ...dataset,
  949. data: []
  950. };
  951. }
  952. return {
  953. ...dataset,
  954. data: dataset.data.map((value, index) => {
  955. // 确保value是一个有效的数值
  956. const y = parseFloat(value);
  957. if (isNaN(y)) {
  958. return { x: index + 1, y: 0, r: 5 };
  959. }
  960. // 气泡大小与值成比例
  961. const r = Math.max(5, Math.min(20, y / 10));
  962. return { x: index + 1, y: y, r: r };
  963. })
  964. };
  965. });
  966. }
  967. /**
  968. * 初始化图表类型预览画廊
  969. */
  970. function initChartTypeGallery() {
  971. // 获取所有图表类型预览项
  972. const chartTypeItems = document.querySelectorAll('.chart-type-item');
  973. // 获取图表类型选择下拉框
  974. const chartTypeSelect = document.getElementById('chart-type');
  975. // 为每个预览项添加点击事件
  976. chartTypeItems.forEach(item => {
  977. item.addEventListener('click', function() {
  978. // 获取图表类型值
  979. const chartType = this.getAttribute('data-chart-type');
  980. // 设置下拉框的值
  981. chartTypeSelect.value = chartType;
  982. // 触发change事件以更新图表
  983. const event = new Event('change');
  984. chartTypeSelect.dispatchEvent(event);
  985. // 更新活动状态
  986. chartTypeItems.forEach(item => item.classList.remove('active'));
  987. this.classList.add('active');
  988. // 如果已经有图表实例,立即生成图表
  989. if (window.chartInstance) {
  990. // 假设generateChart是全局函数
  991. if (typeof window.generateChart === 'function') {
  992. window.generateChart();
  993. }
  994. }
  995. });
  996. });
  997. // 初始化时设置当前选中的图表类型为活动状态
  998. const currentChartType = chartTypeSelect.value;
  999. const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${currentChartType}"]`);
  1000. if (activeItem) {
  1001. activeItem.classList.add('active');
  1002. }
  1003. // 当下拉框选择变化时,同步更新活动预览项
  1004. chartTypeSelect.addEventListener('change', function() {
  1005. const selectedType = this.value;
  1006. chartTypeItems.forEach(item => {
  1007. if (item.getAttribute('data-chart-type') === selectedType) {
  1008. item.classList.add('active');
  1009. } else {
  1010. item.classList.remove('active');
  1011. }
  1012. });
  1013. });
  1014. }