d3绘制图表的常用方法

54 阅读9分钟

d3绘制图表,常见的一些实现效果 1、d3绘制图表的x和y轴

2、d3设置x和y轴的缩放

3、d3根据64*64二维数组绘制热力图矩阵

4、d3自定义色卡缩放

5、d3坐标轴内打点,根据不同类型,加载不同的svg图片。svg图片增加边框(类型不同边框颜色不同)

6、d3根据一组坐标数据,连线绘制区域

7、d3对数坐标系,格式化坐标轴显示 。只显示整数位。例如:10^1 格式化为 10¹ // const superscriptNumbers = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];

8、对数坐标系内,绘制基准直线。表示 t1 = nt2 ;t1表示y轴,t2表示x轴

import * as d3 from 'd3';
import { useEventBus } from '@vueuse/core';
import _ from 'lodash';
import html2canvas from "html2canvas";
import { ElMessage, ElLoading } from 'element-plus';
// const testChartData = {
//     info: { edit: true },
//     x: { range: [0, 331], title: 'c1/c2', addList: [{ field: 'c1', rule: 'max' }, { field: 'c2', rule: 'max' }] },
//     y: { range: [1, 100], type: 'log', title: 'c1', addList: [{ field: 'c1', rule: 'max' }] },
//     polygons: [
//       {
//         points: [[10, 10], [100, 20], [70, 80], [50, 150], [10, 70]],
//         fill: '#00ff00aa',
//         stroke: 1
//       },
//       {
//         points: [[110, 0], [0, 180], [180, 80], [180, 0]],
//         fill: '#ffff00aa',
//         stroke: 1
//       }
//     ],

//     id: 'chart1'
// }
// 组合下载(如果页面同时有D3和Canvas图表)
let loadingInstance = null; 
export async function downloadCombinedChart(svgSelector, canvasSelector, info) {
  // loadingInstance= null;
  // loadingInstance = ElLoading.service({
  //   lock: true,
  //   text: '下载中...',
  //   spinner: 'el-icon-loading',
  //   background: 'rgba(0, 0, 0, 0.8)'
  // });
  // 获取要下载的DOM元素
  const svg = document.querySelector(svgSelector);
  const colorCanvas = document.querySelector(canvasSelector);
  
  if (!svg || !colorCanvas) {
    console.error('SVG或Canvas元素未找到');
    return;
  }
  
  // 将D3的SVG转换为Canvas
  const d3Canvas = await html2canvas(svg);
  
  // 设置padding值
  const padding = {
    top: 40,      // 顶部padding,预留标题空间
    bottom: 40,   // 底部padding,预留说明文字空间
    left: 20,     // 左侧padding
    right: 20     // 右侧padding
  };
  
  // 计算合并后的Canvas尺寸(包含padding)
  const contentWidth = d3Canvas.width + colorCanvas.width + 20; // 20是两个图表之间的间距
  const contentHeight = Math.max(d3Canvas.height, colorCanvas.height);
  
  // 总尺寸 = 内容尺寸 + 左右/上下padding
  const combinedWidth = contentWidth + padding.left + padding.right;
  const combinedHeight = contentHeight + padding.top + padding.bottom;
  
  // 创建新Canvas用于合并
  const combinedCanvas = document.createElement('canvas');
  const mergedCtx = combinedCanvas.getContext('2d');
  combinedCanvas.width = combinedWidth;
  combinedCanvas.height = combinedHeight;
  
  // 绘制白色背景
  mergedCtx.fillStyle = '#ffffff';
  mergedCtx.fillRect(0, 0, combinedWidth, combinedHeight);
  
  // 绘制D3图表(考虑左侧padding)
  mergedCtx.drawImage(d3Canvas, padding.left, padding.top);
  
  // 绘制颜色图例Canvas(靠右放置,考虑padding和间距)
  const colorCanvasX = padding.left + d3Canvas.width + 0; // 20是间距
  const colorCanvasY = padding.top; // 
  // const colorCanvasY = padding.top + (contentHeight - colorCanvas.height); 垂直居中 
  mergedCtx.drawImage(colorCanvas, colorCanvasX, colorCanvasY);
  
  // 添加标题(顶部居中)
  mergedCtx.font = '14px Arial';
  mergedCtx.fillStyle = '#333333';
  mergedCtx.textAlign = 'center';
  mergedCtx.fillText('解释图版('+info.dep+'m)', combinedWidth / 2, padding.top - 10); // 稍高于内容区域
  
  // 添加底部说明文字(左下角)
  mergedCtx.font = '14px Arial';
  mergedCtx.fillStyle = '#333';
  mergedCtx.textAlign = 'left';
  mergedCtx.fillText('(提示:原始数据*100000)', padding.left, combinedHeight - padding.bottom / 2);
  
  // 创建下载链接
  combinedCanvas.toBlob(blob => {
    if (!blob) {
      console.error('无法创建图片blob');
      return;
    }
    
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${info.downName}.png`; // 使用传入的文件名
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    
    // 显示成功消息
    // loadingInstance.close();
    ElMessage.success('下载成功');
  }, 'image/png');
}


function getRandomHexColor() {
	return `#${Math.floor(Math.random() * 0xffffff)
		.toString(16)
		.padStart(6, '0')}`;
}

export function getParamsByRow(row, isDSJ) {
	if (isDSJ) {
		let chartVo = row.chartVo;
		let x = chartVo.shaftVos[0];
		let y = chartVo.shaftVos[1];
		const polygonsNames = [];
		const polygons = [];
		row.chartValueZoneParams = chartVo.zoneVos || [];
		row.chartValueZoneParams.map((e) => {
			const index = polygonsNames.findIndex((p) => p == e.vzName);
			if (index != -1) {
				polygons[index].push(e);
			} else {
				polygonsNames.push(e.vzName);
				polygons.push([e]);
			}
		});
		return {
			info: {},
			jh: row.jh,
			range: row.range,
			x: { range: [x.str ? x.str : 1, x.end], title: x.formula, type: x.flag == 1 ? 'value' : 'log', fields: x.fieldList },
			y: { range: [y.str, y.end], title: y.formula, type: y.flag == 1 ? 'value' : 'log', fields: y.fieldList },
			points: [],
			neighbor: [],
			valsData: row.detail?.vals || [],
			polygons: polygons.map((e) => {
				return {
					points: e.map((p) => [p.xvalue, p.yvalue]),
					fill: 'transprent',
					stroke: 0.5,
					exp: e[0].exp,
					name: e[0].vzName,
				};
			}),

			id: 'chart1',
		};
	}
	if (!row || !row.coordinateShaftParams) {
		console.log('缺少数据');
		return;
	}

	const x = row.coordinateShaftParams.find((e) => e.shaft == 'x');
	const y = row.coordinateShaftParams.find((e) => e.shaft == 'y');
	if (!x || !y) {
		console.log('缺少轴数据');
		return;
	}
	//   console.log('getParamsByRow===', row)
	const polygonsNames = [];
	const polygons = [];
	row.chartValueZoneParams = row.chartValueZoneParams || [];
	row.chartValueZoneParams.map((e) => {
		const index = polygonsNames.findIndex((p) => p == e.vzName);
		if (index != -1) {
			polygons[index].push(e);
		} else {
			polygonsNames.push(e.vzName);
			polygons.push([e]);
		}
	});
	let colors = [
		'#ff4e20',
		'#cca4e3',
		'#ff2d51',
		'#3eede7',
		'#009900',
		'#96ce54',
		'#225599',
		'#e4c6d0',
		'#ed5736',
		'#e9e7ef',
		'#c0ebd7',
		'#bce672',
		'#4c8dae',
	];
	let jhStyle = {};
	if (row.neighborExplainCoordinates?.length) {
		row.neighborExplainCoordinates.forEach((i, index) => {
			i.jh = i.text.split(' ')[0];
			if (!jhStyle[i.jh]) {
				jhStyle[i.jh] = colors[index] || getRandomHexColor();
			}
		});
		row.neighborExplainCoordinates.forEach((i, index) => {
			i.borderColor = jhStyle[i.jh] || 'red';
		});
	}
	return {
		info: {},
		jh: row.jh,
		range: row.range,
		x: { range: [x.str ? x.str : 1, x.end], title: x.formula, type: x.flag == 1 ? 'value' : 'log', fields: x.fieldList },
		y: { range: [y.str, y.end], title: y.formula, type: y.flag == 1 ? 'value' : 'log', fields: y.fieldList },
		points: row.explainCoordinates || [],
		neighbor: row.neighborExplainCoordinates || [],
		polygons: polygons.map((e) => {
			return {
				points: e.map((p) => [p.xvalue, p.yvalue]),
				fill: e[0].vzColor,
				stroke: 0.5,
				exp: e[0].exp,
				name: e[0].vzName,
			};
		}),

		id: 'chart',
	};
}
// 上标数字映射表 (0-9)
const superscriptNumbers = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];

// 格式化函数:将10^3转换为10³格式
export function formatWithSuperscript(d) {
	const exponent = Math.log10(d); // d=1000, exponent=3
	const roundedExponent = Math.round(exponent); //roundedExponent 3
	// 只返回整数指数的标签(避免非10的整数幂显示)
	return Math.abs(exponent - roundedExponent) < 0.001 ? `10${superscriptNumbers[roundedExponent]}` : ''; // 非10的整数幂不显示标签
}
export function isLtzfChart(name) {
	let ltzArr = ['流体组分定量计算图版', '流体组分识别定量计算图版'];
	if (ltzArr.includes(name)) {
		return true;
	}
	return false;
}
function generateData(jzData) {
	const data = [];
	const rows = 64;
	const cols = 64;

	for (let i = 0; i < rows; i++) {
		for (let j = 0; j < cols; j++) {
			// 计算在对数坐标系中的实际值
			const xValue = Math.pow(10, Math.log10(0.5) + ((Math.log10(3000) - Math.log10(0.5)) * j) / cols);
			const yValue = Math.pow(10, Math.log10(1) + ((Math.log10(3000) - Math.log10(1)) * i) / rows);

			// 创建数据点 - 这里使用一个函数生成值
			// const value = Math.sin(xValue / 200) * Math.cos(yValue / 300) + 0.5 * Math.sin(xValue / 50) + 0.3 * Math.cos(yValue / 70);

			// 归一化到0-1范围
			// const normalizedValue = (value + 1.5) / 3;
			const normalizedValue = Number(jzData[i][j]);

			data.push({
				x: xValue,
				y: yValue,
				value: normalizedValue,
			});
		}
	}

	return data;
}
export function initHeatChart({ data, color, svg, xScale, yScale, splitNum, innerWidth, innerHeight }) {
	let jzData = generateData(data);
	const valueExtent = d3.extent(jzData, (d) => d.value); //计算数组中元素的最小值和最大值[min, max]
	// console.log('valueExtent=222=', valueExtent, jzData);
	// 自定义颜色数组(您提供的色值范围)
	let colorArr = [
		'#aa0000', // 橙色
		'#e37a46', // 橙色
		'#e3c646', // 黄色
		'#52a438', // 深绿色1
		'#61e396', // 浅绿色
		'#4687e3', // 蓝色
		'#e346d6', // 粉色
		'#464ee3', // 深蓝色
		'#38a45a', // 深绿色2
	];
	let colorScale = d3.scaleSequential(d3.interpolateTurbo).domain(valueExtent);
	if (color.cols?.length) {
		colorArr = color.cols;
	}
	// let colorReverse = _.reverse(customColors);
	if (color.cols?.length) {
		colorScale = d3.scaleSequential().domain(valueExtent).interpolator(d3.interpolateRgbBasis(colorArr)); // 直接使用此插值函数
	}
	// 创建网格
	const gridSizeX = innerWidth / 64;
	const gridSizeY = innerHeight / 64;

	// 绘制热力单元
	svg
		.selectAll('.heat-cell')
		.data(jzData)
		.enter()
		.append('rect')
		.attr('class', 'heat-cell')
		.attr('x', (d) => xScale(d.x))
		.attr('y', (d) => yScale(d.y) - gridSizeY)
		.attr('width', gridSizeX)
		.attr('height', gridSizeY)
		.attr('fill', (d) => colorScale(d.value))
		.attr('stroke', 'none')
		// .attr("rx", 2) //白色小点
		// .attr("ry", 2)
		.on('mouseover', function (event, d) {
			// 高亮当前单元格
			d3.select(this).attr('stroke', '#fff').attr('stroke-width', 2);

			// 显示工具提示
			d3.select('.tooltip')
				.style('opacity', 1)
				.html(`X: ${d.x.toFixed(2)}<br>Y: ${d.y.toFixed(2)}<br>值: ${d.value}`)
				.style('left', event.pageX + 10 + 'px')
				.style('top', event.pageY - 28 + 'px');
		})
		.on('mouseout', function () {
			// 移除高亮
			d3.select(this).attr('stroke', 'none');
			d3.select('.tooltip').style('opacity', 0);
		});
	// initColorLegend
	initColorCanvas({
		isVertical: true,
		chartId: 'chartLegend',
		min: valueExtent[0],
		max: _.last(valueExtent),
		customColors: colorArr,
		color: color,
		splitNum: splitNum || color.splitNum || 10,
		innerHeight: 510,
		innerWidth: 30,
		canvasHeight: 510,
		canvasWidth: 80,
	});

  // initColorLegend({
	// 	isVertical: true,
	// 	chartId: 'chartLegendd',
	// 	min: valueExtent[0],
	// 	max: _.last(valueExtent),
	// 	customColors: colorReverse,
	// 	color: color,
	// 	splitNum: splitNum || color.splitNum || 10,
	// 	innerHeight: 510,
	// 	innerWidth: 30,
	// 	canvasHeight: 510,
	// 	canvasWidth: 80,
	// });
}
export function initColorCanvas({
	isVertical = false,
	chartId = 'chartLegend',
	min,
	max,
	customColors,
	canvasWidth,
	canvasHeight,
	splitNum = 20,
	color,
	innerWidth,
	innerHeight,
}) {
	const canvas = document.getElementById(chartId);
	if (!canvas) return;

	// 根据方向设置画布尺寸
	if (isVertical) {
		// 垂直方向: 宽度较小,高度较大
		canvas.width = canvasWidth || 80;
		canvas.height = canvasHeight || 300;
	} else {
		// 水平方向保持原有设置
		canvas.width = innerWidth;
		canvas.height = canvasHeight || 80;
	}

	const ctx = canvas.getContext('2d');
	// 清空画布
	ctx.clearRect(0, 0, canvas.width, canvas.height);

	const tickNums = splitNum;
	const step = ((max - min) / tickNums).toFixed(2) - 0;

	// 计算文本宽度偏移
	let minTextWidth = ctx.measureText(min);
	let maxTextWidth = ctx.measureText(max);
	let offsetLeft = isVertical ? 0 : Math.ceil(minTextWidth.width);
	let offsetRight = isVertical ? 10 : Math.ceil(maxTextWidth.width);
	let offsetTop = isVertical ? 10 : 10;
	let offsetBottom = isVertical ? 10 : Math.ceil(minTextWidth.actualBoundingBoxAscent) + innerHeight;

	// 设置字体
	ctx.font = '10px Arial';

	if (isVertical) {
		// 垂直方向绘制逻辑
		const heightRect = Math.ceil(canvas.height - offsetTop - offsetBottom);
		const rectWidth = innerWidth || 30;

		// 创建从下到上的渐变色
		const gradient = ctx.createLinearGradient(0, canvas.height - offsetBottom, 0, offsetTop);
		for (let i = 0; i < customColors.length; i++) {
			// 垂直方向颜色分布与水平相反,需要反转比例
      let offset = (i/(customColors.length - 1));
			let colorp = customColors[i];
			gradient.addColorStop(offset, colorp);
		}

		// 绘制色卡矩形
		ctx.fillStyle = gradient;
		ctx.fillRect(offsetLeft, offsetTop, rectWidth, heightRect);

		// 绘制刻度线和数值
		const divider = Math.ceil(tickNums / 10); // 每10个格子显示一个刻度值
		const itemHeight = (heightRect / tickNums).toFixed(2) - 0;
		const startX = offsetLeft + rectWidth; // 刻度线起始X坐标

		// 绘制右侧基线
		ctx.beginPath();
		// 设置线段颜色和宽度等样式(可选)
		ctx.strokeStyle = '#333';
		ctx.lineWidth = 1;
		ctx.moveTo(startX, offsetTop);
		ctx.lineTo(startX, offsetTop + heightRect);
		ctx.stroke();

		// 绘制每个刻度
		for (let i = 0; i < tickNums + 1; i++) {
			// 计算当前刻度的Y坐标(从下往上)
			const scaleY = canvas.height - offsetBottom - i * itemHeight;

			ctx.beginPath();
			ctx.moveTo(startX, scaleY);

			// 计算刻度值
			let textVal = Math.round(min + i * step);
			if (step < 10) {
				textVal = (min + i * step).toFixed(2) - 0;
			}

			// 每隔一定间隔绘制刻度线和数值
			if (i % divider === 0) {
				const metrics = ctx.measureText(textVal);
				const textX = startX + 18; // 文本在刻度线右侧
				const textY = scaleY + metrics.actualBoundingBoxAscent / 2;

				// 绘制较长的刻度线
				ctx.lineTo(startX + 14, scaleY);
				// 绘制文本
				ctx.fillText(textVal, textX, textY);
			} else {
				// 绘制较短的刻度线
				ctx.lineTo(startX + 8, scaleY);
			}
			ctx.stroke();
		}
		// 清除之前的事件监听,避免重复绑定
		//  canvas.removeEventListener('click', handleClick);
		// 添加点击事件监听
		canvas.addEventListener('click', () => {
			useEventBus('showColorDialog').emit({
				min: min,
				max: max,
				splitNum: splitNum,
				name: color.name,
				id: color.id,
				// colorInfo: color
			});
		});
	} else {
		// 水平方向保持原有逻辑
		const widthRect = Math.ceil(innerWidth - offsetRight - offsetLeft);

		// 绘制矩形渐变色的色卡
		const gradient = ctx.createLinearGradient(offsetLeft, 0, innerWidth - offsetRight, innerHeight);
		for (let i = 0; i < customColors.length; i++) {
			// let pr = (i * (1 / customColors.length)).toFixed(2) - 0;
      let offset = (i/(customColors.length - 1));
			gradient.addColorStop(offset, customColors[i]);
		}

		ctx.fillStyle = gradient;
		ctx.fillRect(offsetLeft, 0, widthRect, innerHeight);

		// 绘制刻度值
		const divider = Math.ceil(tickNums / 10);
		const itemWidth = (widthRect / tickNums).toFixed(2) - 0;
		const startY = innerHeight;

		ctx.beginPath();
		ctx.moveTo(offsetLeft, startY);
		ctx.lineTo(widthRect + offsetLeft, startY);

		for (let i = 0; i < tickNums + 1; i++) {
			const scaleNumber = i * itemWidth + offsetLeft;
			ctx.moveTo(scaleNumber, startY);

			if (i % divider === 0) {
				let textVal = Math.round(min + i * step);
				if (step < 10) {
					textVal = (min + i * step).toFixed(2) - 0;
				}
				const metrics = ctx.measureText(textVal);
				const textX = scaleNumber - (metrics.width / 2).toFixed(0) - 0;
				ctx.fillText(textVal, textX, startY + 30);
				ctx.lineTo(scaleNumber, startY + 14);
			} else {
				ctx.lineTo(scaleNumber, startY + 8);
			}
		}
		ctx.stroke();
	}
}

// export function initColorCanvas({isVertial = false, chartId, min, max, customColors,canversHeight, splitNum = 20, color, innerWidth, innerHeight}){
//   const canvas = document.getElementById(chartId);
//   canvas.width = innerWidth;
//   canvas.height = canversHeight || 80;
//   if (!canvas) return
//   const ctx = canvas.getContext('2d')

//   // 画之前清空画布
//   ctx.clearRect(0, 0, canvas.width, canvas.height);

//   const tickNums = splitNum;
//   const step = ((max - min) / tickNums).toFixed(2) - 0
// console.log('step==', step);
//   let minTextWidth = ctx.measureText(min)
//   let maxTextWidth = ctx.measureText(max)
//   let offsetLeft = Math.ceil(minTextWidth.width) // 画布左偏移
//   let offsetRight = Math.ceil(maxTextWidth.width) // 画布右偏移

//   const widthRect = Math.ceil(innerWidth - offsetRight - offsetLeft)

//   // 绘制矩形渐变色的色卡
//   var gradient = ctx.createLinearGradient(offsetLeft, 0, innerWidth - offsetRight, innerHeight)
//   for (var i = 0; i < customColors.length; i++) {
//     let pr = (i * (1 / customColors.length)).toFixed(2) - 0
//     let colorp = customColors[i]
//     gradient.addColorStop(pr, colorp) // 起始颜色为红色
//   }

//   // gradient.addColorStop(0.5, "blue");  // 中间颜色为蓝色
//   // gradient.addColorStop(1, "green");  // 结束颜色为绿色
//   ctx.fillStyle = gradient
//   ctx.fillRect(offsetLeft, 0, widthRect, innerHeight)

//   // 设置字体
//   ctx.font = '14px Arial'

//   // 绘制刻度值
//   const divider = Math.ceil(tickNums / 10) //每十个格子就写一次刻度数字
//   const itemWidth = (widthRect / tickNums).toFixed(2) - 0 // 隔十个像素就画一个刻度线
//   const startY = innerHeight // 画刻度线的起始y坐标

//   ctx.moveTo(offsetLeft, startY)
//   ctx.lineTo(widthRect + offsetLeft, startY)
//   // ctx.stroke();

//   for (let i = 0; i < tickNums + 1; i++) {
//     const scaleNumber = i * itemWidth + offsetLeft
//     ctx.moveTo(scaleNumber, startY)
//     // 开头偏移的像素
//     if (i % divider === 0) {
//       let textVal = Math.round(min + i * step)
//       if (step < 10) {
//         textVal = (min + i * step).toFixed(2) - 0
//       }
//       const metrics = ctx.measureText(textVal)
//       const textX = scaleNumber - (metrics.width / 2).toFixed(0) - 0
//       // console.log(textX);
//       ctx.fillText(textVal, textX, startY + 30)
//       ctx.lineTo(scaleNumber, startY + 14)
//     } else {
//       ctx.lineTo(scaleNumber, startY + 8)
//     }
//   }
//   ctx.stroke()
// }
export function initColorLegend({ isVertial = true, chartId, min, max, customColors, splitNum = 20, color, canvasHeight, innerWidth, innerHeight }) {
	//  添加颜色图例
	let id = chartId || 'chartLegend';
	const parent = document.getElementById(id);

	const width = parent.getBoundingClientRect().width;
	const height = parent.getBoundingClientRect().height;
  const paddingTop = isVertial ? 10 : 0;
  const paddingBottom = 10;
	d3.select('#' + id)
		.selectAll('*')
		.remove();

	const legendSvg = d3
		.select('#' + id)
		.append('svg')
		.attr('width', width)
		.attr('height', height);

	const legendGroup = legendSvg.append('g').attr('transform', `translate(${0}, ${paddingTop})`);

	// 颜色条
	const colorBarHeight = canvasHeight - paddingTop - paddingBottom;
	const colorBarWidth = innerWidth || 500;

	const gradientId = `color-gradient-${Date.now()}`;

	legendGroup
		.append('rect')
		.attr('width', isVertial ? 30 : colorBarWidth)
		.attr('height', isVertial ? colorBarHeight : 30)
		.attr('fill', `url(#${gradientId})`);

	// 颜色渐变定义
	const defs = legendSvg.append('defs');

	let gradient = defs
		.append('linearGradient')
		.attr('id', gradientId)
		.attr('x1', '0%')
		.attr('y1', !isVertial ? '50%' : '0%')
		.attr('x2', !isVertial ? '100%' : '0%')
		.attr('y2', !isVertial ? '50%' : '100%'); // 水平:从左到下右变;垂直:从下到上

	// 为每个自定义颜色添加渐变节点
	customColors.forEach((item, index) => {
		const offset = (index / (customColors.length - 1)) * 100;
		gradient.append('stop').attr('offset', `${offset}%`).attr('stop-color', item);
	});

	// 颜色条刻度
	const legendScale = d3
		.scaleLinear()
		.domain([min, max])
		.range(isVertial ? [colorBarHeight, 0] : [0, colorBarWidth]);

	// 关键修复1:手动生成精确的刻度值(避免D3自动优化)
	const tickValues = d3.range(min, max, (max - min) / splitNum);
	tickValues[0] = min;
	tickValues[tickValues.length - 1] = max;
	// if (tickValues.length < 2) tickValues.push(max);
	// 确保包含最大值(避免末尾刻度缺失)
	// if (!tickValues.includes(max)) tickValues.push(max);
	// 计算主刻度间隔,确保主刻度数量合理
	// const majorTickInterval = Math.ceil(splitNum / 3); // 每5个次刻度显示1个主刻度
	// 关键修复2:动态计算主刻度间隔(基于实际刻度数量)
	const totalTicks = tickValues.length;
	const majorTickCount = Math.max(2, Math.min(5, Math.floor(totalTicks / 5))); // 主刻度最多5个
	const majorTickInterval = Math.ceil(totalTicks / majorTickCount);
	console.log('tickValues==', tickValues, majorTickCount, majorTickInterval);
	let tickLength = 10;
	let tickInnerLength = 4;
	// 创建刻度生成器
	let legendAxis;
	if (isVertial) {
		legendAxis = d3
			.axisRight(legendScale)
			//  .tickValues(tickValues)
			.ticks(splitNum)
			.tickSize(tickLength)
			.tickSizeInner(tickInnerLength)
			.tickFormat((d, i) => {
				// 只在主刻度位置显示标签
				return i % majorTickInterval === 0 ? d.toFixed(2) : '';
			});
	} else {
		legendAxis = d3
			.axisBottom(legendScale)
			.tickValues(tickValues)
			.tickSize(tickLength)
			.tickSizeInner(tickInnerLength)
			// 设置主刻度线长度为8px,次刻度线长度为tickInnerLength px
			.tickFormat((d, i) => {
				// 只在主刻度位置显示标签
				return i % majorTickInterval === 0 ? d.toFixed(2) : '';
			});
	}

	legendGroup
		.append('g')
		.attr('class', 'axis color-axis')
		.attr('transform', isVertial ? `translate(30, 0)` : 'translate(0, 30)')
		.call(legendAxis);

	// 关键修复3:强制设置刻度线样式(避免被CSS覆盖)
	legendGroup
		.selectAll('.tick line')
		.style('stroke', '#666') // 刻度线颜色
		.style('stroke-width', 1); // 刻度线粗细

	// 防止标签重叠
	legendGroup
		.selectAll('.tick text')
		.style('font-size', '10px')
		.attr('fill', '#333')
		.attr('text-anchor', (d, i) => {
			// 首标签强制左对齐,尾标签强制右对齐,中间默认
			if (i === 0) return 'start';
			if (i === tickValues.length - 1) return 'end';
			return isVertial ? 'start' : 'middle';
		})
		.attr('dx', (d, i) => {
			// 首标签偏移修正,尾标签反向偏移
			if (i === 0) return isVertial ? '0.2em' : '-0.5em';
			if (i === tickValues.length - 1) return isVertial ? '-0.2em' : '0.5em';
			return isVertial ? '0.5em' : '0em';
		})
		.attr('dy', isVertial ? '0.3em' : '1em');

	legendGroup.style('cursor', 'pointer');
	legendGroup.on('click', function () {
		// 切换到下一个主题
		useEventBus('showColorDialog').emit({
			min: min,
			max: max,
			splitNum: splitNum,
			name: color.name,
			id: color.id,
			// colorInfo: color
		});
	});
	// 颜色条标题
	// legendGroup
	//   .append("text")
	//   .attr("class", "axis-title color-legend")
	//   .attr("x", 15)
	//   .attr("y", 10)
	//   .attr("text-anchor", "middle")
	//   .text(color.name);
}

export function tbChart(params, extra = {}) {
	// console.log('tbChart params==', params)
	const { id, dom, x, y, polygons, points, neighbor, info, valsData } = params;
	const jh = params.jh;
	const wellRange = params.range;
	if (x.type == 'log' && x.range[0] == 0) {
		x.range[0] = 0.1;
	}
	if (y.type == 'log' && y.range[0] == 0) {
		y.range[0] = 0.1;
	}
	const parent = document.getElementById(id);

	const width = parent.getBoundingClientRect().width;
	const height = parent.getBoundingClientRect().height;

	d3.select('#' + id)
		.selectAll('*')
		.remove();

	const svg = d3
		.select('#' + id)
		.append('svg')
		.attr('width', width)
		.attr('height', height);

	let dif = 50; ///id === 'chart1' ? 41.23 :50
	if (id === 'chart1') {
		dif = 57.23;
	}
	const xScale =
		x.type === 'log'
			? d3
					.scaleLog()
					.domain([x.range[0], x.range[1]])
					.range([dif, width - dif])
			: d3
					.scaleLinear()
					.domain([x.range[0], x.range[1]])
					.range([dif, width - dif]);

	// Y 轴尺度:根据传递的类型选择线性刻度或对数刻度
	const yScale =
		y.type === 'log'
			? d3
					.scaleLog()
					.domain([y.range[0], y.range[1]])
					.range([height - dif, 10])
			: d3
					.scaleLinear()
					.domain([y.range[0], y.range[1]])
					.range([height - dif, 10]);

	const xticks = xScale.ticks(x.type === 'log' ? 3 : 5);
	const yticks = yScale.ticks(y.type === 'log' ? 3 : 5);

	// x、y轴对数
	const logAxis = () => {
		//    const x0 = Math.log10(0.5);
		//     const x1 = Math.log10(3000);
		//     const y0 = Math.log10(1);
		// xScale = d3.scaleLinear()
		//     .domain([x0, x1])
		//     .range([dif, width - dif]);
		//     d3.scaleLinear().domain([y.range[0], y.range[1]]).range([height - dif, 10])
		// yScale = d3.scaleLinear()
		//     .domain([y0, x1])
		//     .range([dif, height - dif]);
		// const ticks = [];
		// for (let k = Math.ceil(x0); k <= Math.floor(x1); k++) {
		//     ticks.push(k);
		// }
		// 创建坐标轴,使用自定义刻度
		const xAxis = d3
			.axisBottom(xScale)
			.ticks(6, 'e') // 自动生成刻度
			.tickFormat((d) => formatWithSuperscript(d)); // 显示为指数形式
		const yAxis = d3
			.axisLeft(yScale)
			.ticks(6, 'e') // 自动生成刻度
			.tickFormat((d) => formatWithSuperscript(d));
		// 绘制坐标轴
		svg
			.append('g')
			.attr('class', 'yAxis')
			.attr('transform', `translate(0, ${height - dif})`)
			.call(xAxis);
		svg.append('g').attr('transform', `translate(${dif}, 0)`).call(yAxis);

		// 绘制参考线(t1 = k*t2)
		const referenceLines = [
			{ k: 1, color: '#FCE800', label: 'T1 = T2', start: [y.range[0], y.range[0]], end: [y.range[1], y.range[1]] },
			{ k: 6, color: '#FCE800', label: 'T1 = 6×T2' },
			{ k: 20, color: '#FCE800', label: 'T1 = 20×T2' },
			{ k: 200, color: '#FCE800', label: 'T1 = 200×T2' },
		];

		referenceLines.forEach((line, i) => {
			if (line.k == 1) {
				// linePoints.push(1.05, 1.05);
				// linePoints.push(1, 1);
				// 绘制T1=T2直线
				svg
					.append('line')
					.attr('x1', xScale(line.start[0]))
					.attr('y1', yScale(line.start[1])) // T1=T2,所以y等于x
					.attr('x2', xScale(line.end[0]))
					.attr('y2', yScale(line.end[1]))
					.attr('stroke', line.color)
					.attr('stroke-dasharray', i % 2 === 0 ? '0' : '6,4') // 只有T1=T2用实线
					.attr('stroke-width', 1.2);
				// .attr('stroke-opacity', 0.8);
				return;
			}
			// 生成线上的点
			const linePoints = [];
			for (let t2 = x.range[0]; t2 <= x.range[1]; t2 *= 1.1) {
				const t1 = line.k * t2;
				if (t1 <= y.range[1] && t1 >= y.range[0]) {
					linePoints.push({ t1, t2 });
				}
			}
			for (let t1 = y.range[0]; t1 <= y.range[1]; t1 *= 1.1) {
				const t2 = t1 / line.k;
				if (t1 > y.range[0] && t2 > x.range[0] && t1 > 2999) {
					linePoints.push({ t1, t2 });
				}
			}

			// 绘制线
			svg
				.append('path')
				.datum(linePoints)
				.attr('class', 'reference-line')
				.attr('fill', 'none')
				.attr('stroke', line.color)
				.attr('stroke-width', 1.2)
				.attr('stroke-dasharray', i % 2 === 0 ? '0' : '6,4') // 只有T1=T2用实线
				.attr(
					'd',
					d3
						.line()
						.x((d) => xScale(d.t2))
						.y((d) => yScale(d.t1))
				);

			// 添加线标签
			// const lastPoint = linePoints[linePoints.length - 1];
			// svg.append("text")
			//     .attr("class", "line-label")
			//     .attr("x", xScale(lastPoint.t2) + 5)
			//     .attr("y", yScale(lastPoint.t1) + 5)
			//     .attr("fill", line.color)
			//     .text(line.label);
		});
	};
	console.log('isLtzfChart(extra.subtypeName)==', isLtzfChart(extra.subtypeName));
	if (isLtzfChart(extra.subtypeName) || extra.isDSJ) {
		if (extra.isDSJ) {
			// 大数据图版解释
			initHeatChart({ splitNum: extra.splitNum, color: extra.color, data: valsData, svg, xScale, yScale, innerWidth: width, innerHeight: height });
		}
		logAxis();
	} else {
		svg
			.append('g')
			.attr('transform', `translate(0, ${height - dif})`)
			.call(
				d3
					.axisBottom(xScale)
					.tickValues(xticks) // 设置特定的刻度值
					.tickFormat(d3.format('.1f'))
			); // 格式化刻度值为整数);

		svg
			.append('g')
			.attr('transform', `translate(${dif}, 0)`)
			.call(
				d3
					.axisLeft(yScale)
					.tickValues(yticks) // 设置特定的刻度值
					.tickFormat(d3.format('.1f'))
			); // 格式化刻度值为整数);
	}
	// 绘制纵向网格线
	svg
		.append('g')
		.attr('class', 'grid')
		.attr('transform', `translate(${dif}, 0)`)
		.call(
			d3
				.axisLeft(yScale)
				.tickValues(yticks)
				.tickSize(-width + 2 * dif) // 网格线长度,覆盖整个绘图区域并留出一些边距
				.tickFormat('')
		); // 去掉刻度标签

	// 绘制横向网格线(注意:这里使用了与X轴相同的刻度,但调整了tickSize)
	svg
		.append('g')
		.attr('class', 'grid')
		.attr('transform', `translate(0, ${height - dif})`)
		.call(
			d3
				.axisBottom(xScale)
				.tickValues(xticks)
				.tickSize(-height + dif + 10) // 网格线长度,覆盖整个绘图区域并留出一些边距
				.tickFormat('')
		); // 去掉刻度标签

	// 设置网格线样式
	svg
		.selectAll('.grid line')
		.style('stroke', 'ddddddaa') // 网格线颜色
		.style('stroke-width', 0.5); // 网格线宽度

	// 在X轴中间位置添加文本
	svg
		.append('text')
		.attr('x', width / 2) // X轴中间位置
		.attr('y', height - 20) // 调整Y轴位置
		// .style('font-size', '10px')
		.attr('text-anchor', 'middle') // 文本水平居中
		.text(x.fakeTitle || x.title || 'X')
		.style('cursor', info.edit ? 'pointer' : 'default')
		.on('click', () => {
			if (info.edit) {
				useEventBus('showCalDialog').emit({
					range: x.range,
					title: x.title,
					fields: x.fields,
					addList: x.fields || [
						{
							field: 'c1',
							rule: 'max',
						},
						{
							field: 'c2',
							rule: 'max',
						},
					],
					axis: 'x',
					type: x.type,
				});
			}
		});
	svg
		.append('text')
		.attr('x', 0) // Y轴的X位置
		.attr('y', height / 2) // Y轴的Y中间位置
		// .style('font-size', '10px')
		.attr('text-anchor', 'middle') // 水平居中
		.attr('transform', 'rotate(-90, 10, ' + height / 2 + ')') // 旋转围绕自身
		.text(y.fakeTitle || y.title || 'Y')
		.style('cursor', info.edit ? 'pointer' : 'default')
		.on('click', () => {
			if (info.edit) {
				useEventBus('showCalDialog').emit({
					range: y.range,
					title: y.title,
					fields: y.fields,
					addList: y.fields || [
						{
							field: 'c1',
							rule: 'max',
						},
					],
					axis: 'y',
					type: y.type,
				});
			}
		});

	// console.log('polygons====', polygons);
	// 绘制多个多边形
	polygons.forEach((polygon) => {
		const centerX = polygon.points.reduce((sum, p) => sum + xScale(p[0]), 0) / polygon.points.length;
		const centerY = polygon.points.reduce((sum, p) => sum + yScale(p[1]), 0) / polygon.points.length;
		const fillColor = extra.isDSJ || isLtzfChart(extra.subtypeName) ? 'transparent' : polygon.fill;
		const fillStroke = extra.isDSJ || isLtzfChart(extra.subtypeName) ? '#FF4500	' : polygon.stroke;
		const strokeWidth = extra.isDSJ || isLtzfChart(extra.subtypeName) ? 1.2 : polygon.strokeWidth;
		svg
			.append('polygon')
			.attr('points', polygon.points.map((d) => xScale(d[0]) + ',' + yScale(d[1])).join(' '))
			// .attr('points', polygonData.points.map(d => xScale(d[0]) + ',' + yScale(d[1]) + ','))
			.attr('fill', fillColor || 'steelblue')
			.attr('stroke', fillStroke || 'black')
			.attr('stroke-width', strokeWidth || 0.5)
			.append('title') // 添加 title 元素
			.text(polygon.name);
		if (!extra.hidePolygonName) {
			svg
				.append('text')
				.attr('x', centerX)
				.attr('y', centerY)
				// .attr('dy', -10)  // 微调文本的垂直位置
				.attr('text-anchor', 'middle')
				.attr('font-size', 14)
				.attr('fill', 'black')
				.text(polygon.name);
		}
	});

	if (neighbor) {
		neighbor.forEach((point) => {
			if (point.x && point.y) {
				let getZone = checkPointZone([point.x, point.y], polygons);
				const tooltip = d3
					.select('body')
					.append('div')
					.attr('class', 'myd3_tooltip')
					.style('position', 'absolute')
					// .style("display", "none")
					.style('display', 'block')
					.style('background', 'rgba(0,0,0,0.8)')
					.style('color', 'white')
					.style('padding', '8px')
					.style('z-index', '9999')
					.style('border-radius', '4px')
					.style('pointer-events', 'none')
					.html(point.text); // 井号 层位范围
				if (!getZone?.exp) {
					const imgBox = svg
						.append('circle')
						.attr('cx', xScale(point.x)) // 圆心的 x 坐标
						.attr('cy', yScale(point.y)) // 圆心的 y 坐标
						.attr('r', 4) // 圆的半径
						.attr('fill', point.color ? point.color : 'red') // 圆的填充颜色
						.attr('stroke', point.borderColor || '#000') // 圆的边框颜色
						.attr('stroke-width', 1); // 圆的边框宽度

					imgBox
						.on('mouseover', function (event, d) {
							tooltip
								.style('display', 'block')
								.style('left', `${event.x + 10}px`)
								.style('top', `${event.y + 10}px`);
						})
						.on('mouseout', function () {
							tooltip.style('display', 'none');
						})
						.on('click', function () {
							tooltip.style('display', 'none');
						});
					return;
				}
				// console.log('neighbor getZone.exp==', getZone.exp, point);
				d3.xml(`assets/ljjs_svg/${getZone.exp}.svg`).then((r) => {
					const wrapper = r.documentElement.cloneNode(true);
					let transX = 9;
					if (id === 'chart1') {
						transX = 14;
					}
					const imgBox = svg
						.append('g')
						.attr('transform', `translate(${xScale(point.x) - transX}, ${yScale(point.y) - 9})`)
						.append('svg')
						.attr('width', 14)
						.attr('height', 14)
						.attr('preserveAspectRatio', 'xMidYMid meet'); // 保持比例

					imgBox.node().appendChild(wrapper);
					imgBox
						.append('rect') // 添加矩形作为边框容器
						.attr('width', 14)
						.attr('height', 14)
						.attr('fill', 'none') // 透明填充
						.attr('stroke', point.borderColor || '#000')
						.attr('stroke-width', 2);
					imgBox
						.on('mouseover', function (event, d) {
							tooltip
								.style('display', 'block')
								.style('left', `${event.x + 10}px`)
								.style('top', `${event.y + 10}px`);
						})
						.on('mouseout', function () {
							tooltip.style('display', 'none');
						})
						.on('click', function () {
							tooltip.style('display', 'none');
						});
				});
			}
		});
	}

	var currentZone = null;
	if (points) {
		points.forEach((point) => {
			// console.log('111points===', point);
			if (point.x && point.y) {
				const tooltip = d3
					.select('body')
					.append('div')
					.attr('class', 'myd3_tooltip')
					.style('position', 'absolute')
					.style('display', 'block')
					.style('background', 'rgba(0,0,0,0.8)')
					.style('color', 'white')
					.style('padding', '8px')
					.style('border-radius', '4px')
					.style('z-index', '9999')
					.style('pointer-events', 'none')
					.html(point.text ? point.text : jh + '  ' + wellRange); // 井号 层位范围
				currentZone = checkPointZone([point.x, point.y], polygons);
				if (!currentZone?.exp) {
					const imgBox = svg
						.append('circle')
						.attr('cx', xScale(point.x)) // 圆心的 x 坐标
						.attr('cy', yScale(point.y)) // 圆心的 y 坐标
						.attr('r', 4) // 圆的半径
						.attr('fill', point.color ? point.color : 'steelblue') // 圆的填充颜色
						.attr('stroke', point.borderColor || '#000') // 圆的边框颜色
						.attr('stroke-width', 1); // 圆的边框宽度

					imgBox
						.on('mouseover', function (event, d) {
							console.log('tooltip', tooltip, point);
							tooltip
								.style('display', 'block')
								.style('left', `${event.x + 10}px`)
								.style('top', `${event.y + 10}px`);
						})
						.on('mouseout', function () {
							tooltip.style('display', 'none');
						})
						.on('click', function () {
							tooltip.style('display', 'none');
						});

					return;
				}
				// console.log('currentZone.exp==', currentZone.exp)
				d3.xml(`assets/ljjs_svg/${currentZone.exp}.svg`).then((r) => {
					const wrapper = r.documentElement.cloneNode(true);
					let transX = 9;
					if (id === 'chart1') {
						transX = 14;
					}
					const imgBox = svg
						.append('g')
						.attr('transform', `translate(${xScale(point.x) - transX}, ${yScale(point.y) - 9})`)
						.append('svg')
						.attr('width', 14)
						.attr('height', 14)
						.attr('preserveAspectRatio', 'xMidYMid meet'); // 保持比例

					imgBox.node().appendChild(wrapper);
					imgBox
						.append('rect') // 添加矩形作为边框容器
						.attr('width', 14)
						.attr('height', 14)
						.attr('fill', 'none') // 透明填充
						.attr('stroke', point.borderColor || '#000')
						.attr('stroke-width', 2);
					imgBox
						.on('mouseover', function (event, d) {
							tooltip
								.style('display', 'block')
								.style('left', `${event.x + 10}px`)
								.style('top', `${event.y + 10}px`);
						})
						.on('mouseout', function () {
							tooltip.style('display', 'none');
						})
						.on('click', function () {
							tooltip.style('display', 'none');
						});
				});
			}
		});
	}
	return { currentZone };
}
//判断点在哪个价值区
export function checkPointZone(point, polygons) {
	for (let i = 0; i < polygons.length; i++) {
		if (d3.polygonContains(polygons[i].points, point)) {
			return polygons[i]; // 返回当前价值区所有属性name、exp
		}
	}
	return null;
}