antvG2折线图和区间range标记同时绘制

效果

line和range都能通过tooltip显示数据,让line和range共用一套坐标轴,并且当鼠标移动到line上时,tooltip显示line的数据信息;当鼠标移动到range上,tooltip显示range的数据信息

我试过用spaceLayer,让line和range分别在一个view上,但会出现两个坐标轴,而又因为line和range数据结构不一致,所以无法共用一套数据,只能分别设置数据,数据的不同,导致scale比例尺对应不上,坐标轴也对应不上。因此,spaceLayer的方案放弃。

下面是g2chartHook

TypeScript 复制代码
//g2chartHook.ts
import { ref, onMounted, onUnmounted, onBeforeUnmount } from "vue";
import { Chart } from "@antv/g2";
import { Renderer as SVGRenderer } from "@antv/g-svg";

let option: any = null;
let chart: Chart | null = null;
let viewArea = null;
let plotArea = null;
let mainArea = null;
let contentArea = null;
let chartRect: any = null;
let viewAreaRect = null;
let plotAreaRect = null;
let mainAreaRect = null;
let contentAreaRect = null;
let zhamenRangePath = null;
let zhamenYArr = [3946, 4096];
let zhamenSVGList = [];
let zhamenPositionY = 0;

/**
 * 获取闸门SVG元素并计算其位置信息
 * 该函数用于从G2图表中提取闸门相关的SVG元素,并计算其Y轴位置
 */
function getZhamenRangeSVG() {
    const svgElement = document.querySelector("#g-root");//找到ID为g-root的g2图表svg元素
	console.log("[DEBUG] svgElement", svgElement);
	if (svgElement) {
		const pathElements = svgElement.querySelectorAll('path.element');//选取所有class为element的path标签
		console.log("[DEBUG] Found path elements:", pathElements);
		// 遍历所有path元素,找到height属性不为空的
		for (const path of pathElements) {
			const height = path.getAttribute("height");
			if (Number(height) > 0) {
				//console.log("[DEBUG] Found path with fill:", path);
				zhamenSVGList.push(path); // 将找到的闸门SVG元素添加到数组中
			}
		}
		console.log('zhamenSVGList', zhamenSVGList);

		plotArea = svgElement.children[0]; // 获取绘图区域
		mainArea = plotArea.children[plotArea.children.length - 1]; // 获取主区域
		console.log("[DEBUG] mainArea", mainArea);
		const matrix = mainArea.getAttribute("transform"); // 获取变换矩阵
		console.log("[DEBUG] matrix", matrix);
		// 从变换矩阵中提取顶部边距值
		const paddingTop = matrix?.slice(matrix.lastIndexOf(',')+1, matrix.length-1);
		console.log('paddingTop', paddingTop);
		// 计算闸门的Y轴位置:顶部边距 + 闸门元素高度
		zhamenPositionY = Number(paddingTop) + Math.ceil(Number(zhamenSVGList[0].getAttribute("height")));
	}
}

/**
 * 初始化1124图表
 * 
 * @param {HTMLElement} dom - 图表容器DOM元素
 * @param {any[]} bolist - 波形数据/折线数据
 * @param {any[]} rangeData - 范围数据
 * @returns {Chart} 返回初始化后的图表实例
 * 
 * @description
 * 该函数用于创建并初始化一个1124图表实例,使用SVG渲染器。
 * 图表会自动适应容器大小,并基于提供的波形数据和范围数据进行渲染。
 * 渲染完成后会获取图表容器的位置信息和闸门范围SVG元素。
 */
export function init1124Chart(
	dom: HTMLElement,
	bolist: any[],
	rangeData: any[],
) {
	chart = new Chart({
		container: dom,
		autoFit: true,
		// height: 400,
		renderer: new SVGRenderer({ outputSVGElementId: true }),
	});

	
	chart.options(buildWaveOption(bolist, rangeData));

	chart.render().then(() => {
		// console.log("render done");

		chartRect = dom.getBoundingClientRect();
		// console.log("chartRect", chartRect);
		getZhamenRangeSVG();
		console.log("gate h", zhamenSVGList[0].getAttribute("height"));
		
	});
	return chart;
}
/**
 * 组装g2图表选项
 * @param bolist 
 * @param rangeData 
 * @returns 
 */
function buildWaveOption(bolist: any[], rangeData: any[]) {
	option = {
		type: "view",
		children: [customTooltip(buildLineOption(bolist), rangeData), ...buildRangeOption(rangeData)],
	};

	return option;
}
/**
 * 构建折线图选项
 * @param bolist 
 * @returns 
 */
function buildLineOption(bolist: any[]) {
	let lineOption = {
		type: "line",
		data: bolist,
		encode: {
			x: "x",
			y: "y",
			shape: "smooth",
		},
		style: {
			stroke: "#667eea",
			lineWidth: 2,
		},
		scale: {
			x: {
				// min: 100,
				// max: 1124,
				nice: false,
				type: "linear", // 指定类型
				domain: [100, 1124], // 指定定义域
				range: [0, 1], // 指定值域
				tickMethod: (min, max, count) => {
					let tickCount = 16;
					let tickStep = (1124 - 100) / (tickCount - 1);
					let ticks = [];
					for (let i = 0; i < tickCount; i++) {
						ticks.push(Number((100 + tickStep * i).toFixed(0)));
					}
					return ticks;
				},
			},
			y: {
				// min: 0,
				// max: 4096,
				nice: false,
				type: "linear", // 指定类型
				domain: [0, 4096], // 指定定义域
				range: [1, 0], // 指定值域
				tickMethod: (min, max, count) => {
					let tickCount = 5;
					let tickStep = (4096 - 0) / (tickCount - 1);
					let ticks = [];
					for (let i = 0; i < tickCount; i++) {
						ticks.push(Number((0 + tickStep * i).toFixed(0)));
					}
					return ticks;
				},
			},
		},
		axis: {
			x: {
				title: false,
				titleFontSize: 14,
				titleFontWeight: "bold",
				labelAutoRotate: false,
				line: {
					lineWidth: 2,
					stroke: "#101010",
				},
				grid: {
					stroke: "#e0e0e0",
					lineWidth: 2,
					lineDash: [4, 4],
				},
			},
			y: {
				title: false,
				titleFontSize: 14,
				titleFontWeight: "bold",
				line: {
					lineWidth: 2,
					stroke: "#101010",
				},
				grid: {
					stroke: "#e0e0e0",
					lineWidth: 2,
					lineDash: [4, 4],
				},
			},
		},
		//不能设置tooltip,否则interaction的自定义tooltip会失效
		// tooltip: {
        //   title: (d) => `位置 ${d.x}`,
        //   items: [{ field: "y", name: "大小" }],
		// }
      
	};

	return lineOption;
}
/**
 * 自定义提示窗体
 * @param lineOption 
 * @param rangeData 
 * @returns 
 */
function customTooltip(lineOption: any, rangeData: any[]) {

	let tooltip = {
		interaction: {
          tooltip: {
            
            // 方法1: 在interaction tooltip配置中使用render函数,实现当鼠标悬停在range内时,显示range的tooltip;不在range内时,显示line的tooltip
            render: (event, { items, title }) => {
            //   console.log("render", items, title);
            //   console.log("render",event.x,event.y,event.offsetX,event.offsetY,event.clientX,event.clientY);
			  
              let dataX = title;
              
              console.log("zhamenPositionY", event.y , zhamenPositionY);
              
			  if(Array.isArray(rangeData) && event.y < zhamenPositionY) {
				//当鼠标悬停在闸门1内时,显示闸门1的tooltip
				if(rangeData.length > 0 && dataX >= rangeData[0].x[0] && dataX <= rangeData[0].x[1]) {
					return generateTooltipHtml(
						'','位置',`${rangeData[0].x[0]} - ${rangeData[0].x[1]}`,
					);
				} 
				//当鼠标悬停在闸门2内时,显示闸门2的tooltip
				else if(rangeData.length > 1 && dataX >= rangeData[1].x[0] && dataX <= rangeData[1].x[1]) {
					return generateTooltipHtml(
						'', '位置',`${rangeData[1].x[0]} - ${rangeData[1].x[1]}`,
					);
				}
			  }
			  //当鼠标悬停在line上时,显示line的tooltip
              return generateTooltipHtml(`位置 ${title}`, '大小',items[0].value);
            },
          },
        }
	}

	Object.assign(lineOption, tooltip);
	return lineOption;
}
/**
 * 生成提示窗体html
 * @param title 
 * @param name 
 * @param value 
 * @returns 
 */
function generateTooltipHtml(title, name, value) {
  //最上层div不能用g2-tooltip样式,否则会出现一小一大两个tooltip框,很难看
  let html = `
            <div>
              <div class="g2-tooltip-title">${title}</div>
              <ul class="g2-tooltip-list">
                <li class="g2-tooltip-list-item">
        <span class="g2-tooltip-list-item-name" 
        style="display: flex; align-items: center; max-width: 216px;">
          <span class="g2-tooltip-list-item-marker" 
          style="background: rgb(102, 126, 234); width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px;"></span>
          <span class="g2-tooltip-list-item-name-label" 
          title="大小" style="flex: 1 1 0%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;">${name}</span>
        </span>
        <span class="g2-tooltip-list-item-value" 
        title="2322" style="display: inline-block; float: right; flex: 1 1 0%; text-align: right; min-width: 28px; margin-left: 30px; color: rgba(0, 0, 0, 0.85); overflow: hidden; white-space: nowrap; text-overflow: ellipsis;"
        >${value}</span>
      </li></ul></div>
            `;
  return html;
}
/**
 * 构建范围图选项
 * @param rangeData 
 * @returns 
 */
function buildRangeOption(rangeData: any[]) {
	if(!rangeData) return [];

	let rangeOption = {
		type: "range",
		data: rangeData,
		encode: { x: "x", y: "y", color: "label" },
		//labels: [{ text: "label", style: { fill: "#fff", dy: 0 } }],
		tooltip: false,
		// zIndex: 1,
		// axis: false,
		scale: {
			color: {
				range: ["#667eea", "#f1ab14"], //设置闸门标记的颜色
			},
		},
		legend: {
			color: {
				position: "bottom",
				itemMarker: "rect",//图例项图标形状为矩形
				itemMarkerSize: 20,//图例项图标大小
				layout: {
					justifyContent: "center", // 主轴(水平)居中
				},
			},
		},
	};
	let lineXArr = [];
	for (const element of rangeData) {
		lineXArr.push({
			type: "lineX",
			data: [element.x[0]],
			style: { stroke: "#000", strokeOpacity: 0.45, lineDash: [3, 4] },
			labels: [
				{
					text: element.x[0],
					position: "right",//辅助线文本位置是镜像的,左右相反
					dy: -140,//默认辅助线文本居中显示,上移140px,正好在闸门矩形下沿
					fill: "#000",
				},
			],
		});
		lineXArr.push({
			type: "lineX",
			data: [element.x[1]],
			style: { stroke: "#000", strokeOpacity: 0.45, lineDash: [3, 4] },
			labels: [
				{
					text: element.x[1],
					position: "left",
					dy: -140,
					fill: "#000",
				},
			],
		});
	}

	return [rangeOption, ...lineXArr];
}

export function buildXdataFor1124(length: number, index: number) {
	//将x轴从100到1124等分为length-1份
	return Number((((1124 - 100) / (length - 1)) * index + 100).toFixed(0));
}
export function buildRangeDataFor1124({
	zhamen1,
	zhamen2,
}: {
	zhamen1: number[];
	zhamen2: number[];
}) {
	let range = [];
	if (
		Array.isArray(zhamen1) &&
		zhamen1.length === 2 &&
		Array.isArray(zhamen2) &&
		zhamen2.length === 2
	) {
		range.push({
			x: [zhamen1[0], zhamen1[1]],
			y: [zhamenYArr[0], zhamenYArr[1]],
			label: "闸门1",
			color: "#667eea",
		});

		if (zhamen2[0] < 1124) {
			//如果闸门2的开始位置在1124之前,那么结束位置不能超过1124
			let z2 = zhamen2[1] > 1124 ? 1124 : zhamen2[1];
			range.push({
				x: [zhamen2[0], z2],
				y: [zhamenYArr[0], zhamenYArr[1]],
				label: "闸门2",
				color: "#f1ab14",
			});
		}
		//如果闸门2的开始位置在1124之后,不绘制闸门2
	}

	return range.length > 0 ? range : null;
}
/**
 * 更新范围图
 * @param rangeData 
 */
export function updateRange(rangeData: any[]) {
	// 获取所有 Mark 实例
	// const marks = chart?.getMarks();//获取不到  G2 5.x版本用函数调用方式基本废弃了,option配置是主流
	// console.log('获取mark实例', marks)
	// 根据类型查找特定的 Mark (例如 interval)
	// const range = marks.find(mark => mark.getType() === 'range');
	// console.log('获取mark实例', marks, range)

	chart?.clear();
	
	option.children = [option.children[0], ...buildRangeOption(rangeData)];
	chart?.options(option);
	chart?.render().then(() => {
		console.log('闸门重绘完成')
	});

}

onBeforeUnmount(() => {
	// 销毁图表实例
	if (chart) {
		chart.destroy();
	}
});

调用时,`init1124Chart(dom, lineData, [{x:[20, 30], y: [100, 200]}])`

相关推荐
德育处主任Pro2 小时前
从重复搭建到高效生产,RollCode的H5开发新范式
前端
蜡台2 小时前
SPA(Single Page Application) Web 应用(即单页应用)架构模式 更新
前端·架构·vue·react·spa·spa更新
网络点点滴3 小时前
组件通信-作用域插槽
前端·javascript·vue.js
LZQ <=小氣鬼=>4 小时前
React 图片放大镜组件使用文档
javascript·react.js·前端框架·ecmascript
kyriewen114 小时前
异步编程:从“回调地狱”到“async/await”的救赎之路
开发语言·前端·javascript·chrome·typescript·ecmascript·html5
早點睡3904 小时前
ReactNative项目Openharmony三方库集成实战:@react-native-clipboard/clipboard
javascript·react native·react.js
Old Uncle Tom4 小时前
Markdown Viewer 再升级
前端
吴声子夜歌4 小时前
JavaScript——数据类型
开发语言·javascript·ecmascript
Luna-player4 小时前
Vue3中使用vue-awesome-swiper
前端·vue.js·arcgis