12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130 |
- // 图表实例,方便后续更新
- window.chartInstance = null;
- // 注册Chart.js插件
- if (Chart && Chart.register) {
- // 如果有ChartDataLabels插件,注册它
- if (window.ChartDataLabels) {
- Chart.register(ChartDataLabels);
- }
- }
- // 生成图表的主函数
- function createChart(data, settings) {
- // 获取Canvas元素
- const canvas = document.getElementById('chart-canvas');
- const ctx = canvas.getContext('2d');
-
- // 如果已有图表,先销毁
- if (window.chartInstance) {
- window.chartInstance.destroy();
- }
-
- // 应用颜色方案
- applyColorScheme(data, settings.colorScheme, settings.type);
-
- // 配置图表选项
- const options = getChartOptions(settings);
-
- // 处理特殊图表类型
- let type = settings.type;
- let chartData = {...data};
- // 检查是否为首系列图表类型
- const isFirstSeriesOnly = settings.type.includes(" (首系列)");
- if (isFirstSeriesOnly) {
- // 提取真正的图表类型
- type = settings.type.replace(" (首系列)", "");
-
- // 只保留第一个数据系列
- if (chartData.datasets.length > 1) {
- const firstDataset = chartData.datasets[0];
- chartData.datasets = [{
- ...firstDataset,
- label: firstDataset.label || '数据'
- }];
- }
- }
- // 移除可能存在的旧堆叠设置
- if (options.scales && options.scales.x) {
- delete options.scales.x.stacked;
- }
- if (options.scales && options.scales.y) {
- delete options.scales.y.stacked;
- }
- // 移除旧的填充设置和其他特殊属性
- chartData.datasets.forEach(dataset => {
- delete dataset.fill;
- delete dataset.tension;
- delete dataset.stepped;
- delete dataset.borderDash;
- });
- // 基本图表类型处理
- switch(type) {
- // 柱状图系列
- case 'horizontalBar':
- type = 'bar';
- options.indexAxis = 'y';
- break;
- case 'stackedBar':
- type = 'bar';
- if (!options.scales) options.scales = {};
- if (!options.scales.x) options.scales.x = {};
- if (!options.scales.y) options.scales.y = {};
- options.scales.x.stacked = true;
- options.scales.y.stacked = true;
- break;
- case 'groupedBar':
- type = 'bar';
- // 分组柱状图是默认行为
- break;
- case 'gradientBar':
- type = 'bar';
- // 渐变效果在applyColorScheme函数中处理
- break;
- case 'barWithError':
- type = 'bar';
- // 添加误差线
- chartData.datasets.forEach(dataset => {
- dataset.errorBars = {
- y: {
- plus: dataset.data.map(() => Math.random() * 5 + 2),
- minus: dataset.data.map(() => Math.random() * 5 + 2)
- }
- };
- });
- break;
- case 'rangeBar':
- type = 'bar';
- // 转换数据为范围格式
- chartData.datasets.forEach(dataset => {
- dataset.data = dataset.data.map(value => {
- const min = Math.max(0, value - Math.random() * value * 0.4);
- return [min, value];
- });
- });
- break;
-
- // 线/面积图系列
- case 'area':
- type = 'line';
- chartData.datasets.forEach(dataset => {
- dataset.fill = true;
- });
- break;
- case 'curvedLine':
- type = 'line';
- chartData.datasets.forEach(dataset => {
- dataset.tension = 0.4; // 更平滑的曲线
- });
- break;
- case 'stepLine':
- type = 'line';
- chartData.datasets.forEach(dataset => {
- dataset.stepped = true;
- });
- break;
- case 'stackedArea':
- type = 'line';
- chartData.datasets.forEach(dataset => {
- dataset.fill = true;
- });
- if (!options.scales) options.scales = {};
- if (!options.scales.y) options.scales.y = {};
- options.scales.y.stacked = true;
- break;
- case 'streamgraph':
- type = 'line';
- // 流图效果:堆叠面积图 + 居中对齐
- chartData.datasets.forEach(dataset => {
- dataset.fill = true;
- });
- if (!options.scales) options.scales = {};
- if (!options.scales.y) options.scales.y = {};
- options.scales.y.stacked = true;
- options.scales.y.offset = true; // 居中对齐堆叠
- break;
- case 'timeline':
- type = 'line';
- chartData.datasets.forEach(dataset => {
- dataset.stepped = 'before';
- dataset.borderDash = [5, 5]; // 虚线效果
- });
- break;
-
- // 饼图/环形图系列
- case 'halfPie':
- type = 'doughnut';
- options.circumference = Math.PI;
- options.rotation = -Math.PI / 2;
- break;
- case 'nestedPie':
- type = 'doughnut';
- // 嵌套效果通过多个饼图叠加实现,简化实现仅调整内外半径
- if (chartData.datasets.length > 0) {
- chartData.datasets[0].radius = '70%';
- chartData.datasets[0].weight = 0.7;
- }
- break;
-
- // 散点/气泡图系列
- case 'scatter':
- chartData.datasets = transformScatterData(chartData.datasets);
- break;
- case 'bubble':
- chartData.datasets = transformBubbleData(chartData.datasets);
- type = 'bubble';
- break;
- case 'scatterSmooth':
- chartData.datasets = transformScatterData(chartData.datasets);
- type = 'scatter';
- // 添加趋势线
- chartData.datasets.forEach(dataset => {
- const smoothedDataset = {
- ...dataset,
- type: 'line',
- data: [...dataset.data],
- pointRadius: 0,
- tension: 0.4,
- fill: false
- };
- chartData.datasets.push(smoothedDataset);
- });
- break;
-
- // 专业图表系列
- case 'funnel':
- type = 'bar';
- // 简化的漏斗图实现
- options.indexAxis = 'y';
- if (chartData.datasets.length > 0) {
- // 对数据进行排序
- const sortedData = [...chartData.datasets[0].data].sort((a, b) => b - a);
- chartData.datasets[0].data = sortedData;
- // 确保Y轴反转
- if (!options.scales) options.scales = {};
- if (!options.scales.y) options.scales.y = {};
- options.scales.y.reverse = true;
- }
- break;
- case 'gauge':
- type = 'doughnut';
- // 简化的仪表盘实现
- if (chartData.datasets.length > 0 && chartData.datasets[0].data.length > 0) {
- const value = chartData.datasets[0].data[0];
- const max = Math.max(...chartData.datasets[0].data) * 1.2;
- const remainder = max - value;
-
- chartData.datasets[0].data = [value, remainder];
- chartData.datasets[0].backgroundColor = ['#36A2EB', '#E0E0E0'];
- chartData.labels = ['Value', ''];
-
- options.circumference = Math.PI;
- options.rotation = -Math.PI;
- options.cutout = '70%';
- }
- break;
- case 'boxplot':
- // 简化的箱线图实现(基于柱状图)
- type = 'bar';
- // 转换数据为箱线图格式
- chartData.datasets.forEach(dataset => {
- dataset.data = dataset.data.map(value => {
- const q1 = Math.max(0, value * 0.7);
- const median = value * 0.85;
- const q3 = value * 1.15;
- const min = Math.max(0, q1 - (median - q1));
- const max = q3 + (q3 - median);
- return [min, q1, median, q3, max];
- });
- });
- break;
- case 'waterfall':
- type = 'bar';
- // 瀑布图实现
- if (chartData.datasets.length > 0) {
- const data = chartData.datasets[0].data;
- let cumulative = 0;
-
- // 创建新的数据数组,包含每个点的起点和终点
- const waterfallData = data.map((value, index) => {
- const start = cumulative;
- cumulative += value;
- return {
- start: start,
- end: cumulative,
- value: value
- };
- });
-
- // 转换为柱状图数据
- chartData.datasets[0].data = waterfallData.map(d => d.end - d.start);
-
- // 添加起点数据集
- chartData.datasets.push({
- label: '起点',
- data: waterfallData.map(d => d.start),
- backgroundColor: 'rgba(0,0,0,0)',
- borderColor: 'rgba(0,0,0,0)',
- stack: 'waterfall'
- });
-
- // 设置为堆叠柱状图
- if (!options.scales) options.scales = {};
- if (!options.scales.x) options.scales.x = {};
- if (!options.scales.y) options.scales.y = {};
- options.scales.x.stacked = true;
- options.scales.y.stacked = true;
- }
- break;
- case 'treemap':
- case 'sunburst':
- case 'sankey':
- case 'chord':
- case 'network':
- // 这些高级图表需要专门的库支持,这里简化为提示信息
- type = 'bar';
- if (chartData.datasets.length > 0) {
- // 显示一个提示信息
- chartData.datasets = [{
- label: `${settings.type}需要专门的图表库支持`,
- data: [100],
- backgroundColor: '#f8d7da'
- }];
- chartData.labels = ['请尝试其他图表类型'];
- }
- break;
- }
- // 热力图特殊处理
- if (type === 'heatmap') {
- // 热力图不是Chart.js的标准类型,需要使用插件或自定义渲染
- // 简单实现一个基于颜色渐变的矩阵图
- type = 'matrix';
- renderHeatmap(ctx, chartData, options);
- return;
- }
- // 饼图、环形图和极地面积图特殊处理 (如果不是由其他类型转换而来的)
- if (['pie', 'doughnut', 'polarArea'].includes(type) && !['halfPie', 'nestedPie', 'gauge'].includes(settings.type) && !isFirstSeriesOnly) {
- // 如果有多个数据集,只取第一个
- if (chartData.datasets.length > 1) {
- const firstDataset = chartData.datasets[0];
- chartData.datasets = [{
- ...firstDataset,
- label: undefined // 这些图表类型不需要数据集标签
- }];
- }
- }
- // 创建图表实例
- window.chartInstance = new Chart(ctx, {
- type: type,
- data: chartData,
- options: options
- });
-
- // 设置标记属性,表示图表已渲染,去除背景
- canvas.setAttribute('data-chart-rendered', 'true');
-
- return window.chartInstance;
- }
- // 辅助函数:获取当前图表实例
- function getChartInstance() {
- return window.chartInstance;
- }
- // 辅助函数:设置当前图表实例
- function setChartInstance(instance) {
- window.chartInstance = instance;
- }
- // 获取图表配置选项
- function getChartOptions(settings) {
- const options = {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- title: {
- display: !!settings.title,
- text: settings.title,
- font: {
- size: 18,
- weight: 'bold'
- },
- padding: {
- top: 10,
- bottom: 20
- }
- },
- legend: {
- display: settings.legendPosition !== 'none',
- position: settings.legendPosition === 'none' ? 'top' : settings.legendPosition,
- labels: {
- usePointStyle: true,
- padding: 15,
- font: {
- size: 12
- }
- }
- },
- tooltip: {
- enabled: true,
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
- titleFont: {
- size: 14
- },
- bodyFont: {
- size: 13
- },
- padding: 10,
- displayColors: true
- }
- },
- animation: {
- duration: settings.animateChart ? 1000 : 0,
- easing: 'easeOutQuart'
- }
- };
-
- // 检查是否为简单数据模式(只有一个数据集)
- const isSimpleData = settings.isSimpleData ||
- (window.chartData && window.chartData.datasets && window.chartData.datasets.length === 1);
-
- // 如果是简单数据模式,隐藏图例
- if (isSimpleData) {
- options.plugins.legend.display = false;
- }
-
- // 只有部分图表类型需要轴线配置
- if (!['pie', 'doughnut', 'polarArea'].includes(settings.type.replace(" (首系列)", ""))) { // 兼容(首系列)后缀
- options.scales = {
- x: {
- title: {
- display: !!settings.xAxisLabel,
- text: settings.xAxisLabel,
- font: {
- size: 14,
- weight: 'bold'
- },
- padding: {
- top: 10
- }
- },
- grid: {
- display: settings.showGridLines,
- color: 'rgba(0, 0, 0, 0.1)'
- },
- ticks: {
- font: {
- size: 12
- }
- }
- },
- y: {
- title: {
- display: !!settings.yAxisLabel,
- text: settings.yAxisLabel,
- font: {
- size: 14,
- weight: 'bold'
- },
- padding: {
- bottom: 10
- }
- },
- grid: {
- display: settings.showGridLines,
- color: 'rgba(0, 0, 0, 0.1)'
- },
- ticks: {
- font: {
- size: 12
- },
- beginAtZero: true
- }
- }
- };
-
- // 水平柱状图X和Y轴配置需要互换
- if (settings.type === 'horizontalBar') {
- const temp = options.scales.x;
- options.scales.x = options.scales.y;
- options.scales.y = temp;
- }
- }
-
- // 数据标签配置
- if (settings.showDataLabels) {
- options.plugins.datalabels = {
- display: true,
- color: function(context) {
- const actualType = settings.type.replace(" (首系列)", "");
- const dataset = context.dataset;
-
- // 首先检查数据集是否有自定义的datalabels配置
- if (dataset.datalabels && dataset.datalabels.color) {
- const labelColors = dataset.datalabels.color;
-
- // 如果color是数组,则使用对应索引的颜色
- if (Array.isArray(labelColors)) {
- return labelColors[context.dataIndex] || '#333333';
- }
- // 如果color是单个颜色值
- return labelColors;
- }
-
- // 如果没有自定义配置,则使用智能检测
- // 为饼图和环形图使用对比色
- if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
- // 获取背景色
- const index = context.dataIndex;
- const backgroundColor = dataset.backgroundColor[index];
-
- // 计算背景色的亮度
- return isColorDark(backgroundColor) ? '#ffffff' : '#000000';
- } else if (actualType === 'bar' || actualType === 'horizontalBar' ||
- actualType === 'stackedBar' || actualType === 'gradientBar') {
- // 柱状图系列也需要对比色
- let backgroundColor;
-
- // 背景色可能是数组或单个颜色
- if (Array.isArray(dataset.backgroundColor)) {
- backgroundColor = dataset.backgroundColor[context.dataIndex];
- } else {
- backgroundColor = dataset.backgroundColor;
- }
-
- return isColorDark(backgroundColor) ? '#ffffff' : '#333333';
- } else {
- // 其他图表类型使用默认深色
- return '#333333';
- }
- },
- align: function(context) {
- const dataset = context.dataset;
- // 使用数据集中的align配置(如果有的话)
- if (dataset.datalabels && dataset.datalabels.align) {
- return dataset.datalabels.align;
- }
-
- // 默认配置
- const chartType = settings.type.replace(" (首系列)", "");
- if (['line', 'area', 'scatter', 'bubble'].includes(chartType)) {
- return 'top';
- }
- return 'center';
- },
- font: {
- weight: 'bold'
- },
- formatter: function(value, context) {
- const actualType = settings.type.replace(" (首系列)", "");
-
- // 饼图、环形图和极地面积图显示百分比
- if (['pie', 'doughnut', 'polarArea'].includes(actualType)) {
- // 计算百分比
- const dataset = context.chart.data.datasets[context.datasetIndex];
- const total = dataset.data.reduce((total, value) => total + value, 0);
- const percentage = ((value / total) * 100).toFixed(1) + '%';
-
- // 对于较小的扇区只显示百分比,否则显示值和百分比
- const percent = value / total * 100;
- if (percent < 5) {
- return percentage;
- } else {
- return `${value} (${percentage})`;
- }
- }
-
- // 对散点图特殊处理
- if (settings.type === 'scatter') {
- if (context && context.dataset && context.dataset.data &&
- context.dataset.data[context.dataIndex] &&
- typeof context.dataset.data[context.dataIndex].y !== 'undefined') {
- return context.dataset.data[context.dataIndex].y;
- }
- return '';
- }
-
- return value;
- }
- };
- }
-
- return options;
- }
- // 判断颜色是否为深色
- function isColorDark(color) {
- // 处理rgba格式
- if (color && color.startsWith('rgba')) {
- const parts = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
- if (parts) {
- const r = parseInt(parts[1]);
- const g = parseInt(parts[2]);
- const b = parseInt(parts[3]);
- // 计算亮度 (根据人眼对RGB的敏感度加权)
- const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
- return brightness < 0.7; // 亮度小于0.7认为是深色
- }
- }
-
- // 处理rgb格式
- if (color && color.startsWith('rgb(')) {
- const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
- if (parts) {
- const r = parseInt(parts[1]);
- const g = parseInt(parts[2]);
- const b = parseInt(parts[3]);
- const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
- return brightness < 0.7;
- }
- }
-
- // 处理十六进制格式
- if (color && color.startsWith('#')) {
- color = color.replace('#', '');
- const r = parseInt(color.length === 3 ? color.substring(0, 1).repeat(2) : color.substring(0, 2), 16);
- const g = parseInt(color.length === 3 ? color.substring(1, 2).repeat(2) : color.substring(2, 4), 16);
- const b = parseInt(color.length === 3 ? color.substring(2, 3).repeat(2) : color.substring(4, 6), 16);
- const brightness = (r * 0.299 + g * 0.587 + b * 0.114) / 255;
- return brightness < 0.7;
- }
-
- // 默认返回true,使用白色文本
- return true;
- }
- // 应用颜色方案
- function applyColorScheme(data, colorScheme, chartType) {
- // 定义颜色方案 - 全新设计,确保各个方案风格迥异
- const colorSchemes = {
- default: [
- '#4e73df', '#1cc88a', '#36b9cc', '#f6c23e', '#e74a3b',
- '#6f42c1', '#fd7e14', '#20c9a6', '#36b9cc', '#858796'
- ],
- pastel: [
- '#FFB6C1', '#FFD700', '#98FB98', '#87CEFA', '#FFA07A',
- '#DDA0DD', '#FFDAB9', '#B0E0E6', '#F0E68C', '#E6E6FA'
- ],
- bright: [
- '#FF1E1E', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF',
- '#FF00FF', '#FF7F00', '#FF1493', '#00FA9A', '#7B68EE'
- ],
- cool: [
- '#5F4B8B', '#42BFDD', '#00A7E1', '#00344B', '#143642',
- '#0F8B8D', '#4CB5F5', '#1D3557', '#A8DADC', '#457B9D'
- ],
- warm: [
- '#FF7700', '#FF9E00', '#FFCF00', '#FFF400', '#E20000',
- '#D91A1A', '#A60000', '#FF5252', '#FF7B7B', '#FFBF69'
- ],
- corporate: [
- '#003F5C', '#2F4B7C', '#665191', '#A05195', '#D45087',
- '#F95D6A', '#FF7C43', '#FFA600', '#004D40', '#00695C'
- ],
- contrast: [
- '#000000', '#E63946', '#457B9D', '#F1C40F', '#2ECC71',
- '#9B59B6', '#1ABC9C', '#F39C12', '#D35400', '#7F8C8D'
- ],
- rainbow: [
- '#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF',
- '#4B0082', '#9400D3', '#FF1493', '#00FFFF', '#FF00FF'
- ],
- earth: [
- '#5D4037', '#795548', '#A1887F', '#4E342E', '#3E2723',
- '#33691E', '#558B2F', '#7CB342', '#8D6E63', '#6D4C41'
- ],
- ocean: [
- '#006064', '#00838F', '#0097A7', '#00ACC1', '#00BCD4',
- '#26C6DA', '#4DD0E1', '#80DEEA', '#01579B', '#0277BD'
- ],
- vintage: [
- '#8D8741', '#659DBD', '#DAAD86', '#BC986A', '#FBEEC1',
- '#605B56', '#837A75', '#9E8B8B', '#D8C3A5', '#E8DDCD'
- ]
- };
-
- // 获取选定的颜色方案
- const colors = colorSchemes[colorScheme] || colorSchemes.default;
-
- const actualChartType = chartType.replace(" (首系列)", ""); // 获取基础类型
- // 为每个数据集应用颜色
- data.datasets.forEach((dataset, index) => {
- const color = colors[index % colors.length];
-
- // 设置不同图表类型的颜色
- if (['pie', 'doughnut', 'polarArea', 'halfPie', 'nestedPie', 'gauge'].includes(actualChartType)) {
- // 这些图表类型需要为每个数据点设置不同颜色
- // 对于gauge特殊处理,不使用这种方式
- if (actualChartType === 'gauge' && dataset.backgroundColor) {
- // 保留gauge的特殊颜色设置
- } else {
- dataset.backgroundColor = dataset.data.map((_, i) => colors[i % colors.length]);
- dataset.borderColor = 'white';
- dataset.borderWidth = 1;
-
- // 为每个扇区添加对应的前景色(用于数据标签)
- dataset.datalabels = {
- color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#000000')
- };
- }
- } else if (['line', 'area', 'stackedArea', 'curvedLine', 'stepLine', 'timeline', 'streamgraph'].includes(actualChartType)) {
- // 折线图和面积图样式
- dataset.borderColor = color;
- // 根据图表类型调整透明度
- let alpha = 0.1; // 默认折线图半透明
- if (['area', 'stackedArea', 'streamgraph'].includes(actualChartType)) {
- alpha = 0.3; // 面积图相对更不透明
- }
- dataset.backgroundColor = hexToRgba(color, alpha);
- dataset.pointBackgroundColor = color;
- dataset.pointBorderColor = '#fff';
- dataset.pointHoverBackgroundColor = '#fff';
- dataset.pointHoverBorderColor = color;
-
- // 特殊线型
- if (actualChartType === 'curvedLine') {
- dataset.tension = 0.4;
- } else if (actualChartType === 'stepLine') {
- dataset.stepped = true;
- } else if (actualChartType === 'timeline') {
- dataset.stepped = 'before';
- dataset.borderDash = [5, 5];
- } else {
- dataset.tension = 0.3;
- }
-
- // 设置数据标签颜色
- // 对于线图和面积图,标签通常放在点的上方,使用与线条相同的颜色
- dataset.datalabels = {
- color: isColorDark(color) ? color : '#333333',
- align: 'top'
- };
- } else if (actualChartType === 'radar') {
- // 雷达图样式
- dataset.borderColor = color;
- dataset.backgroundColor = hexToRgba(color, 0.2);
- dataset.pointBackgroundColor = color;
- dataset.pointBorderColor = '#fff';
-
- // 雷达图数据标签颜色 - 使用与边框相同的颜色
- dataset.datalabels = {
- color: isColorDark(color) ? color : '#333333'
- };
- } else if (['scatter', 'bubble', 'scatterSmooth'].includes(actualChartType)) {
- // 散点图样式
- dataset.backgroundColor = color;
- dataset.borderColor = hexToRgba(color, 0.8);
-
- // 散点平滑图特殊处理
- if (actualChartType === 'scatterSmooth' && dataset.type === 'line') {
- dataset.borderColor = color;
- dataset.backgroundColor = 'transparent';
- }
-
- // 散点图数据标签颜色
- dataset.datalabels = {
- color: isColorDark(color) ? '#ffffff' : '#333333',
- align: 'top'
- };
- } else if (actualChartType === 'gradientBar') {
- // 渐变柱状图
- const ctx = document.createElement('canvas').getContext('2d');
- const gradient = ctx.createLinearGradient(0, 0, 0, 300);
- gradient.addColorStop(0, color);
- gradient.addColorStop(1, hexToRgba(color, 0.3));
- dataset.backgroundColor = gradient;
- dataset.borderColor = color;
- dataset.borderWidth = 1;
- dataset.hoverBackgroundColor = color;
-
- // 渐变柱状图标签颜色 - 使用顶部颜色判断
- dataset.datalabels = {
- color: isColorDark(color) ? '#ffffff' : '#333333'
- };
- } else if (actualChartType === 'waterfall') {
- // 瀑布图特殊处理
- if (dataset.label === '起点') {
- // 这是为瀑布图添加的起点数据集,保持透明
- } else {
- const values = dataset.data;
- // 根据值的正负设置不同颜色
- const positiveColor = '#36b9cc';
- const negativeColor = '#e74a3b';
-
- dataset.backgroundColor = values.map(value =>
- value >= 0 ? hexToRgba(positiveColor, 0.7) : hexToRgba(negativeColor, 0.7)
- );
- dataset.borderColor = values.map(value =>
- value >= 0 ? positiveColor : negativeColor
- );
- dataset.borderWidth = 1;
-
- // 瀑布图数据标签颜色 - 根据每个柱子的背景色决定
- dataset.datalabels = {
- color: values.map(value =>
- value >= 0 ? (isColorDark(positiveColor) ? '#ffffff' : '#333333') :
- (isColorDark(negativeColor) ? '#ffffff' : '#333333')
- )
- };
- }
- } else if (actualChartType === 'funnel') {
- // 漏斗图特殊处理 - 使用渐变颜色
- const data = dataset.data;
- if (data.length) {
- dataset.backgroundColor = data.map((_, i) => {
- const ratio = 1 - (i / data.length); // 1 到 0
- return hexToRgba(color, 0.5 + ratio * 0.5); // 透明度从1到0.5
- });
- dataset.borderColor = color;
- dataset.borderWidth = 1;
-
- // 漏斗图数据标签颜色 - 根据每个部分的背景色决定
- dataset.datalabels = {
- color: dataset.backgroundColor.map(bgColor => isColorDark(bgColor) ? '#ffffff' : '#333333')
- };
- }
- } else {
- // 默认样式(用于柱状图等)
- dataset.backgroundColor = hexToRgba(color, 0.7);
- dataset.borderColor = color;
- dataset.borderWidth = 1;
- dataset.hoverBackgroundColor = color;
-
- // 默认数据标签颜色 - 根据背景色决定
- dataset.datalabels = {
- color: isColorDark(dataset.backgroundColor) ? '#ffffff' : '#333333'
- };
- }
- });
- }
- // 将十六进制颜色转换为rgba格式
- function hexToRgba(hex, alpha) {
- // 移除井号
- hex = hex.replace('#', '');
-
- // 解析RGB值
- const r = parseInt(hex.length === 3 ? hex.substring(0, 1).repeat(2) : hex.substring(0, 2), 16);
- const g = parseInt(hex.length === 3 ? hex.substring(1, 2).repeat(2) : hex.substring(2, 4), 16);
- const b = parseInt(hex.length === 3 ? hex.substring(2, 3).repeat(2) : hex.substring(4, 6), 16);
-
- // 返回rgba字符串
- return `rgba(${r}, ${g}, ${b}, ${alpha})`;
- }
- // 渲染热力图(自定义实现)
- function renderHeatmap(ctx, data, options) {
- // 清除Canvas
- ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-
- // 设置尺寸和边距
- const margin = {
- top: 50,
- right: 30,
- bottom: 50,
- left: 60
- };
-
- const width = ctx.canvas.width - margin.left - margin.right;
- const height = ctx.canvas.height - margin.top - margin.bottom;
-
- // 获取数据
- const rows = data.labels;
- const columns = data.datasets.map(dataset => dataset.label);
-
- // 创建值矩阵
- const matrix = [];
- rows.forEach((_, rowIndex) => {
- const row = [];
- data.datasets.forEach(dataset => {
- row.push(dataset.data[rowIndex]);
- });
- matrix.push(row);
- });
-
- // 找出最大值和最小值
- const allValues = matrix.flat();
- const min = Math.min(...allValues);
- const max = Math.max(...allValues);
-
- // 绘制标题
- if (options.plugins && options.plugins.title && options.plugins.title.display) {
- ctx.textAlign = 'center';
- ctx.font = '18px Arial';
- ctx.fillStyle = '#333';
- ctx.fillText(options.plugins.title.text, ctx.canvas.width / 2, 25);
- }
-
- // 绘制单元格和标签
- const cellWidth = width / columns.length;
- const cellHeight = height / rows.length;
-
- // 行标签(Y轴)
- ctx.textAlign = 'right';
- ctx.textBaseline = 'middle';
- ctx.font = '12px Arial';
- ctx.fillStyle = '#666';
-
- rows.forEach((label, i) => {
- const y = margin.top + i * cellHeight + cellHeight / 2;
- ctx.fillText(label, margin.left - 10, y);
- });
-
- // 列标签(X轴)
- ctx.textAlign = 'center';
- ctx.textBaseline = 'top';
-
- columns.forEach((label, i) => {
- const x = margin.left + i * cellWidth + cellWidth / 2;
- ctx.fillText(label, x, margin.top + height + 10);
- });
-
- // 绘制热力图单元格
- matrix.forEach((row, i) => {
- row.forEach((value, j) => {
- // 归一化值 (0-1)
- const normalizedValue = (value - min) / (max - min || 1);
-
- // 计算颜色(红-黄-绿渐变)
- const color = getHeatmapColor(normalizedValue);
-
- // 绘制单元格
- const x = margin.left + j * cellWidth;
- const y = margin.top + i * cellHeight;
-
- ctx.fillStyle = color;
- ctx.fillRect(x, y, cellWidth, cellHeight);
-
- // 添加值标签,根据背景色的亮度自动选择标签颜色
- const brightness = getColorBrightness(color);
- ctx.fillStyle = brightness < 0.7 ? 'white' : 'black'; // 亮度阈值为0.7
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.font = 'bold 12px Arial';
- ctx.fillText(value, x + cellWidth / 2, y + cellHeight / 2);
- });
- });
-
- // 绘制坐标轴
- ctx.strokeStyle = '#ddd';
- ctx.lineWidth = 1;
-
- // X轴
- ctx.beginPath();
- ctx.moveTo(margin.left, margin.top + height);
- ctx.lineTo(margin.left + width, margin.top + height);
- ctx.stroke();
-
- // Y轴
- ctx.beginPath();
- ctx.moveTo(margin.left, margin.top);
- ctx.lineTo(margin.left, margin.top + height);
- ctx.stroke();
- }
- // 获取颜色亮度
- function getColorBrightness(color) {
- // 处理rgb格式
- if (color.startsWith('rgb(')) {
- const parts = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
- if (parts) {
- const r = parseInt(parts[1]);
- const g = parseInt(parts[2]);
- const b = parseInt(parts[3]);
- // 计算亮度 (根据人眼对RGB的敏感度加权)
- return (r * 0.299 + g * 0.587 + b * 0.114) / 255;
- }
- }
-
- // 默认返回0.5
- return 0.5;
- }
- // 获取热力图颜色
- function getHeatmapColor(value) {
- // 红-黄-绿渐变
- const r = value < 0.5 ? 255 : Math.round(255 * (1 - 2 * (value - 0.5)));
- const g = value < 0.5 ? Math.round(255 * (2 * value)) : 255;
- const b = 0;
-
- return `rgb(${r}, ${g}, ${b})`;
- }
- // 注册Chart.js插件以支持数据标签
- Chart.register({
- id: 'datalabels',
- beforeDraw: (chart) => {
- const ctx = chart.ctx;
- const options = chart.options.plugins.datalabels;
-
- if (!options || !options.display) {
- return;
- }
-
- chart.data.datasets.forEach((dataset, datasetIndex) => {
- const meta = chart.getDatasetMeta(datasetIndex);
-
- meta.data.forEach((element, index) => {
- // 获取值
- let value = dataset.data[index];
- if (typeof value === 'object' && value !== null) {
- // 散点图等复杂数据结构
- value = value.y;
- }
-
- // 获取位置
- const { x, y } = element.getCenterPoint();
-
- // 确定文本颜色
- let fillColor;
- if (typeof options.color === 'function') {
- fillColor = options.color({
- datasetIndex,
- index,
- dataset,
- dataIndex: index,
- chart: chart
- });
- } else {
- fillColor = options.color || '#666';
- }
-
- ctx.fillStyle = fillColor;
-
- // 设置字体
- ctx.font = options.font.weight + ' 12px Arial';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
-
- // 格式化值
- let text = typeof options.formatter === 'function'
- ? options.formatter(value, {
- datasetIndex,
- index,
- dataset,
- dataIndex: index,
- chart: chart
- })
- : value;
-
- // 绘制文本
- ctx.fillText(text, x, y - 15);
- });
- });
- }
- });
- // 辅助函数:转换散点图数据
- function transformScatterData(datasets) {
- return datasets.map(dataset => {
- if (!dataset.data || !Array.isArray(dataset.data)) {
- return {
- ...dataset,
- data: []
- };
- }
-
- return {
- ...dataset,
- data: dataset.data.map((value, index) => {
- // 确保value是一个有效的数值
- const y = parseFloat(value);
- if (isNaN(y)) {
- return { x: index + 1, y: 0 };
- }
- return { x: index + 1, y: y };
- })
- };
- });
- }
- // 辅助函数:转换气泡图数据
- function transformBubbleData(datasets) {
- return datasets.map(dataset => {
- if (!dataset.data || !Array.isArray(dataset.data)) {
- return {
- ...dataset,
- data: []
- };
- }
-
- return {
- ...dataset,
- data: dataset.data.map((value, index) => {
- // 确保value是一个有效的数值
- const y = parseFloat(value);
- if (isNaN(y)) {
- return { x: index + 1, y: 0, r: 5 };
- }
- // 气泡大小与值成比例
- const r = Math.max(5, Math.min(20, y / 10));
- return { x: index + 1, y: y, r: r };
- })
- };
- });
- }
- /**
- * 初始化图表类型预览画廊
- */
- function initChartTypeGallery() {
- // 获取所有图表类型预览项
- const chartTypeItems = document.querySelectorAll('.chart-type-item');
-
- // 获取图表类型选择下拉框
- const chartTypeSelect = document.getElementById('chart-type');
-
- // 为每个预览项添加点击事件
- chartTypeItems.forEach(item => {
- item.addEventListener('click', function() {
- // 获取图表类型值
- const chartType = this.getAttribute('data-chart-type');
-
- // 设置下拉框的值
- chartTypeSelect.value = chartType;
-
- // 触发change事件以更新图表
- const event = new Event('change');
- chartTypeSelect.dispatchEvent(event);
-
- // 更新活动状态
- chartTypeItems.forEach(item => item.classList.remove('active'));
- this.classList.add('active');
-
- // 如果已经有图表实例,立即生成图表
- if (window.chartInstance) {
- // 假设generateChart是全局函数
- if (typeof window.generateChart === 'function') {
- window.generateChart();
- }
- }
- });
- });
-
- // 初始化时设置当前选中的图表类型为活动状态
- const currentChartType = chartTypeSelect.value;
- const activeItem = document.querySelector(`.chart-type-item[data-chart-type="${currentChartType}"]`);
- if (activeItem) {
- activeItem.classList.add('active');
- }
-
- // 当下拉框选择变化时,同步更新活动预览项
- chartTypeSelect.addEventListener('change', function() {
- const selectedType = this.value;
- chartTypeItems.forEach(item => {
- if (item.getAttribute('data-chart-type') === selectedType) {
- item.classList.add('active');
- } else {
- item.classList.remove('active');
- }
- });
- });
- }
|