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;
}