知识介绍
- 使用两个SVG进行主视图和Len视图的区分,len视图中附带额外信息
代码分析
1 使用两个SVG进行主视图和Len视图的区分
- 设置主视图
// 绘制主SVG的函数const drawMainSVG = () => {// 选择主SVG并设置其属性const svgMain = d3.select(svgMainRef).attr("width", 1000) // 设置宽度.attr("height", 700) // 设置高度.style('border', '1px solid black'); // 设置边框样式// 定义边距const margin = { top: 20, right: 20, bottom: 30, left: 60 };// 计算绘图区域的宽度和高度const width = +svgMain.attr('width') - margin.left - margin.right;const height = +svgMain.attr('height') - margin.top - margin.bottom;// 设置X和Y轴的最小最大值const axisXOffset = 10; // X轴偏移const axisYOffset = 50; // Y轴偏移const xMin = Math.min((d3.min(xpoints(), d => d.coords[0])) - axisXOffset,(d3.min(ypoints(), d => d.coords[0])) - axisXOffset);const xMax = Math.max((d3.max(xpoints(), d => d.coords[0])) + axisXOffset,(d3.min(ypoints(), d => d.coords[0])) + axisXOffset);const yMin = Math.min((d3.min(xpoints(), d => d.coords[1])) - axisYOffset,(d3.min(ypoints(), d => d.coords[1])) - axisYOffset);const yMax = Math.max((d3.max(xpoints(), d => d.coords[1])) + axisYOffset,(d3.min(ypoints(), d => d.coords[1])) + axisYOffset);// 创建X轴和Y轴的比例尺const x = d3.scaleLinear().domain([xMin, xMax]) // 输入数据范围.range([0, width]); // 输出像素范围const y = d3.scaleLinear().domain([yMin, yMax]) // 输入数据范围.range([height, 0]); // 输出像素范围// 创建一个g元素用于绘制内容const g = svgMain.append("g").attr("transform", `translate(${margin.left},${margin.top})`); // 设置内容位置// 绘制X坐标点的圆圈g.selectAll('circle.jsonData').data(xpoints()) // 绑定数据.enter().append('circle') // 创建圆元素.attr('class', 'jsonData') // 设置类名.attr('cx', d => x(d.coords[0])) // 设置圆心X坐标.attr('cy', d => y(d.coords[1])) // 设置圆心Y坐标.attr('r', 2) // 设置圆的半径.attr('fill', 'steelblue'); // 设置圆的填充颜色// 绘制Y坐标点的圆圈g.selectAll('circle.drawingData').data(ypoints()) // 绑定数据.enter().append('circle') // 创建圆元素.attr('class', 'drawingData') // 设置类名.attr('cx', d => x(d.coords[0])) // 设置圆心X坐标.attr('cy', d => y(d.coords[1])) // 设置圆心Y坐标.attr('r', 2) // 设置圆的半径.attr('fill', 'red') // 设置圆的填充颜色.on('mouseover', (event, d) => { // 鼠标悬停事件const tooltip = d3.select('body') // 选择body元素创建tooltip.append('div').attr('class', 'tooltip').style('position', 'absolute') // 绝对定位.style('background-color', 'white') // 背景色.style('border', '1px solid black') // 边框样式.style('padding', '5px') // 内边距.style('pointer-events', 'none') // 不响应鼠标事件.style('left', `${event.pageX + 10}px`) // 设置tooltip左位置.style('top', `${event.pageY + 10}px`) // 设置tooltip顶部位置.text(d.text); // 显示文本内容}).on('mouseout', () => {d3.select('.tooltip').remove(); // 鼠标移出时移除tooltip});// 绘制X坐标点的文本标签g.selectAll('text').data(xpoints()) // 绑定数据.enter().append('text') // 创建文本元素.attr('x', d => x(d.coords[0]) - 8) // 设置文本X位置.attr('y', d => y(d.coords[1]) + 8) // 设置文本Y位置.text(d => d.name) // 设置文本内容.attr('font-family', 'sans-serif') // 设置字体.attr('font-size', '5px') // 设置字体大小.attr('fill', 'black'); // 设置文本颜色// 绘制连接线g.selectAll('line.connection').data(connections()) // 绑定数据.enter().append('line') // 创建线元素.attr('class', 'connection') // 设置类名.attr('x1', d => x(d.X_point[0])) // 连接线起点X.attr('y1', d => y(d.X_point[1])) // 连接线起点Y.attr('x2', d => x(d.Y_transformed_point[0])) // 连接线终点X.attr('y2', d => y(d.Y_transformed_point[1])) // 连接线终点Y.attr('stroke-width', 1) // 线宽.attr('stroke', d => {// 根据条件设置连接线的颜色if (d.matched === 1) {return 'green'; // 匹配的连接线为绿色} else {return 'orange'; // 其他情况使用橙色}});// 绘制X轴和Y轴g.append('g').attr('transform', `translate(0,${height})`) // 设置Y轴位置.call(d3.axisBottom(x)); // 绘制X轴g.append('g').call(d3.axisLeft(y)); // 绘制Y轴// 添加缩放行为const zoomBehavior = zoom().scaleExtent([0.5, 5]) // 设置缩放范围.on('zoom', (event) => {g.attr('transform', event.transform); // 应用缩放变换到g元素updateLenSVG(event.transform); // 更新Len SVG的视图});svgMain.call(zoomBehavior); // 应用缩放行为到主SVG上}
- 设置Len视图
// 绘制辅助SVG的函数const drawLenSVG = () => {const svgLen = d3.select(svgLenRef).attr("width", 500) // 设置宽度.attr("height", 400) // 设置高度.style('margin-left', '10px') // 设置左边距.style('border', '1px solid black'); // 设置边框样式// 创建一个g元素用于放置内容const g = svgLen.append('g');// 复制主SVG中的内容到辅助SVGg.html(d3.select(svgMainRef).select('g').html());// 初始化辅助SVG的视图updateLenSVG(d3.zoomIdentity);};
2.1 对Len视图添加g元素,并复制主SVG中的内容到Len SVG
// 创建一个g元素用于放置内容const g = svgLen.append('g');// 复制主SVG中的内容到辅助SVGg.html(d3.select(svgMainRef).select('g').html());
2.2 初始化LenSVG的视图(函数下一步实现)
updateLenSVG(d3.zoomIdentity);
3. 实现updateLenSVG函数
// 更新辅助SVG的缩放函数function updateLenSVG(transform) {const svgLen = d3.select(svgLenRef); // 选择辅助SVGconst g = svgLen.select('g'); // 选择g元素// 获取主SVG的尺寸const width = +d3.select(svgMainRef).attr('width');const height = +d3.select(svgMainRef).attr('height');// 计算放大区域的边界const centerX = width / 2; // 计算中心X坐标const centerY = height / 2; // 计算中心Y坐标const rectX = (centerX - 250) / transform.k - transform.x / transform.k; // 计算边界Xconst rectY = (centerY - 200) / transform.k - transform.y / transform.k; // 计算边界Y// 设置辅助SVG的视图范围g.attr('transform', `translate(${-rectX * transform.k}, ${-rectY * transform.k}) scale(${transform.k})`);// 更新辅助SVG中的内容g.html(d3.select(svgMainRef).select('g').html());// 重新计算X和Y的最小最大值const axisXOffset = 10; // X轴偏移const axisYOffset = 50; // Y轴偏移const xMin = Math.min((d3.min(xpoints(), d => d.coords[0])) - axisXOffset,(d3.min(ypoints(), d => d.coords[0])) - axisXOffset);const xMax = Math.max((d3.max(xpoints(), d => d.coords[0])) + axisXOffset,(d3.min(ypoints(), d => d.coords[0])) + axisXOffset);const yMin = Math.min((d3.min(xpoints(), d => d.coords[1])) - axisYOffset,(d3.min(ypoints(), d => d.coords[1])) - axisYOffset);const yMax = Math.max((d3.max(xpoints(), d => d.coords[1])) + axisYOffset,(d3.min(ypoints(), d => d.coords[1])) + axisYOffset);// 创建新的比例尺const x = d3.scaleLinear().domain([xMin, xMax]) // 输入范围.range([0, width - 80]); // 输出范围const y = d3.scaleLinear().domain([yMin, yMax]) // 输入范围.range([height - 50, 0]); // 输出范围// 更新Y坐标点的圆圈位置g.selectAll('circle.drawingData').data(ypoints()) // 绑定数据.attr('cx', d => x(d.coords[0])) // 更新X坐标.attr('cy', d => y(d.coords[1])); // 更新Y坐标// 移除旧的文本标签g.selectAll('text.drawingData').remove();// 重新绘制文本标签g.selectAll('circle.drawingData').each(function(d) {const circle = d3.select(this);g.append('text').attr('class','drawingData').attr('x', circle.attr('cx')).attr('y', parseFloat(circle.attr('cy')) - 10).text(d.text).attr('font-family', 'sans-serif').attr('font-size', '10px').attr('fill', 'red').attr('text-anchor', 'middle');});}
3.1 设置Len视图的视图范围,更新内容
const svgLen = d3.select(svgLenRef); // 选择辅助SVGconst g = svgLen.select('g'); // 选择g元素// 获取主SVG的尺寸const width = +d3.select(svgMainRef).attr('width');const height = +d3.select(svgMainRef).attr('height');// 计算放大区域的边界const centerX = width / 2; // 计算中心X坐标const centerY = height / 2; // 计算中心Y坐标const rectX = (centerX - 250) / transform.k - transform.x / transform.k; // 计算边界Xconst rectY = (centerY - 200) / transform.k - transform.y / transform.k; // 计算边界Y// 设置辅助SVG的视图范围g.attr('transform', `translate(${-rectX * transform.k}, ${-rectY * transform.k}) scale(${transform.k})`);// 更新辅助SVG中的内容g.html(d3.select(svgMainRef).select('g').html());
3.2 因为想使redCircles附带新信息,所以重新绘制
请注意:重新绘制len视图的数据时,其相对位置与main视图中存在偏差,所以要手动测试偏移量viewOffetWidth
、viewOffetHeight
。
// 重新计算X和Y的最小最大值const axisXOffset = 10; // X轴偏移const axisYOffset = 50; // Y轴偏移const xMin = Math.min((d3.min(xpoints(), d => d.coords[0])) - axisXOffset,(d3.min(ypoints(), d => d.coords[0])) - axisXOffset);const xMax = Math.max((d3.max(xpoints(), d => d.coords[0])) + axisXOffset,(d3.min(ypoints(), d => d.coords[0])) + axisXOffset);const yMin = Math.min((d3.min(xpoints(), d => d.coords[1])) - axisYOffset,(d3.min(ypoints(), d => d.coords[1])) - axisYOffset);const yMax = Math.max((d3.max(xpoints(), d => d.coords[1])) + axisYOffset,(d3.min(ypoints(), d => d.coords[1])) + axisYOffset);const viewOffetWidth= -80;const viewOffetHeight= -50;// 创建新的比例尺const x = d3.scaleLinear().domain([xMin, xMax]) // 输入范围.range([0, width + viewOffetWidth]); // 输出范围const y = d3.scaleLinear().domain([yMin, yMax]) // 输入范围.range([height + viewOffetHeight, 0]); // 输出范围// 更新Y坐标点的圆圈位置g.selectAll('circle.drawingData').data(ypoints()) // 绑定数据.attr('cx', d => x(d.coords[0])) // 更新X坐标.attr('cy', d => y(d.coords[1])); // 更新Y坐标// 移除旧的文本标签g.selectAll('text.drawingData').remove();// 重新绘制文本标签g.selectAll('circle.drawingData').each(function(d) {const circle = d3.select(this);g.append('text').attr('class','drawingData').attr('x', circle.attr('cx')).attr('y', parseFloat(circle.attr('cy')) - 10).text(d.text).attr('font-family', 'sans-serif').attr('font-size', '10px').attr('fill', 'red').attr('text-anchor', 'middle');});
完整代码
- 使用两个SVG进行主视图和Len视图的区分,len视图中附带额外信息
import { createSignal, onMount } from 'solid-js'; // 导入Solid.js库中的createSignal和onMount
import * as d3 from 'd3'; // 导入D3.js库
import { zoom } from 'd3-zoom'; // 从D3.js导入缩放功能
import { Coordinates, XPoint, YTransformedPoint, Connection, MatchData } from "../utils/type"; // 导入类型定义const Temp = () => {// 创建状态信号用于存储不同类型的数据const [data, setData] = createSignal<MatchData | null>(null); // 存储匹配数据const [xpoints, setXpoints] = createSignal<XPoint[]>([]); // 存储X坐标点const [ypoints, setYpoints] = createSignal<YTransformedPoint[]>([]); // 存储变换后的Y坐标点const [connections, setConnections] = createSignal<Connection[]>([]); // 存储连接信息let svgMainRef: SVGSVGElement | undefined; // 主SVG图形的引用let svgLenRef: SVGSVGElement | undefined; // 辅助SVG图形的引用// 组件挂载时执行的数据加载onMount(async () => {try {// 从指定路径获取数据const response = await fetch('./src/assets/dataset/matched_points_info.json');if (!response.ok) {throw new Error(`HTTP 错误!状态码:${response.status}`); // 若响应不OK则抛出错误}const jsonData: MatchData = await response.json(); // 解析JSON数据setData(jsonData); // 设置状态信号中存储的数据setXpoints(jsonData.X_points); // 设置X坐标点setYpoints(jsonData.Y_transformed_points); // 设置Y坐标点setConnections(jsonData.connections); // 设置连接信息drawMainSVG(); // 调用绘制主SVG的函数drawLenSVG(); // 调用绘制辅助SVG的函数} catch (error) {console.error("加载数据时出错:", error); // 捕获并输出错误信息}});// 绘制主SVG的函数const drawMainSVG = () => {// 选择主SVG并设置其属性const svgMain = d3.select(svgMainRef).attr("width", 1000) // 设置宽度.attr("height", 700) // 设置高度.style('border', '1px solid black'); // 设置边框样式// 定义边距const margin = { top: 20, right: 20, bottom: 30, left: 60 };// 计算绘图区域的宽度和高度const width = +svgMain.attr('width') - margin.left - margin.right;const height = +svgMain.attr('height') - margin.top - margin.bottom;// 设置X和Y轴的最小最大值const axisXOffset = 10; // X轴偏移const axisYOffset = 50; // Y轴偏移const xMin = Math.min((d3.min(xpoints(), d => d.coords[0])) - axisXOffset,(d3.min(ypoints(), d => d.coords[0])) - axisXOffset);const xMax = Math.max((d3.max(xpoints(), d => d.coords[0])) + axisXOffset,(d3.min(ypoints(), d => d.coords[0])) + axisXOffset);const yMin = Math.min((d3.min(xpoints(), d => d.coords[1])) - axisYOffset,(d3.min(ypoints(), d => d.coords[1])) - axisYOffset);const yMax = Math.max((d3.max(xpoints(), d => d.coords[1])) + axisYOffset,(d3.min(ypoints(), d => d.coords[1])) + axisYOffset);// 创建X轴和Y轴的比例尺const x = d3.scaleLinear().domain([xMin, xMax]) // 输入数据范围.range([0, width]); // 输出像素范围const y = d3.scaleLinear().domain([yMin, yMax]) // 输入数据范围.range([height, 0]); // 输出像素范围// 创建一个g元素用于绘制内容const g = svgMain.append("g").attr("transform", `translate(${margin.left},${margin.top})`); // 设置内容位置// 绘制X坐标点的圆圈g.selectAll('circle.jsonData').data(xpoints()) // 绑定数据.enter().append('circle') // 创建圆元素.attr('class', 'jsonData') // 设置类名.attr('cx', d => x(d.coords[0])) // 设置圆心X坐标.attr('cy', d => y(d.coords[1])) // 设置圆心Y坐标.attr('r', 2) // 设置圆的半径.attr('fill', 'steelblue'); // 设置圆的填充颜色// 绘制Y坐标点的圆圈g.selectAll('circle.drawingData').data(ypoints()) // 绑定数据.enter().append('circle') // 创建圆元素.attr('class', 'drawingData') // 设置类名.attr('cx', d => x(d.coords[0])) // 设置圆心X坐标.attr('cy', d => y(d.coords[1])) // 设置圆心Y坐标.attr('r', 2) // 设置圆的半径.attr('fill', 'red') // 设置圆的填充颜色.on('mouseover', (event, d) => { // 鼠标悬停事件const tooltip = d3.select('body') // 选择body元素创建tooltip.append('div').attr('class', 'tooltip').style('position', 'absolute') // 绝对定位.style('background-color', 'white') // 背景色.style('border', '1px solid black') // 边框样式.style('padding', '5px') // 内边距.style('pointer-events', 'none') // 不响应鼠标事件.style('left', `${event.pageX + 10}px`) // 设置tooltip左位置.style('top', `${event.pageY + 10}px`) // 设置tooltip顶部位置.text(d.text); // 显示文本内容}).on('mouseout', () => {d3.select('.tooltip').remove(); // 鼠标移出时移除tooltip});// 绘制X坐标点的文本标签g.selectAll('text').data(xpoints()) // 绑定数据.enter().append('text') // 创建文本元素.attr('x', d => x(d.coords[0]) - 8) // 设置文本X位置.attr('y', d => y(d.coords[1]) + 8) // 设置文本Y位置.text(d => d.name) // 设置文本内容.attr('font-family', 'sans-serif') // 设置字体.attr('font-size', '5px') // 设置字体大小.attr('fill', 'black'); // 设置文本颜色// 绘制连接线g.selectAll('line.connection').data(connections()) // 绑定数据.enter().append('line') // 创建线元素.attr('class', 'connection') // 设置类名.attr('x1', d => x(d.X_point[0])) // 连接线起点X.attr('y1', d => y(d.X_point[1])) // 连接线起点Y.attr('x2', d => x(d.Y_transformed_point[0])) // 连接线终点X.attr('y2', d => y(d.Y_transformed_point[1])) // 连接线终点Y.attr('stroke-width', 1) // 线宽.attr('stroke', d => {// 根据条件设置连接线的颜色if (d.matched === 1) {return 'green'; // 匹配的连接线为绿色} else {return 'orange'; // 其他情况使用橙色}});// 绘制X轴和Y轴g.append('g').attr('transform', `translate(0,${height})`) // 设置Y轴位置.call(d3.axisBottom(x)); // 绘制X轴g.append('g').call(d3.axisLeft(y)); // 绘制Y轴// 添加缩放行为const zoomBehavior = zoom().scaleExtent([0.5, 5]) // 设置缩放范围.on('zoom', (event) => {g.attr('transform', event.transform); // 应用缩放变换到g元素updateLenSVG(event.transform); // 更新Len SVG的视图});svgMain.call(zoomBehavior); // 应用缩放行为到主SVG上}// 绘制辅助SVG的函数const drawLenSVG = () => {const svgLen = d3.select(svgLenRef).attr("width", 500) // 设置宽度.attr("height", 400) // 设置高度.style('margin-left', '10px') // 设置左边距.style('border', '1px solid black'); // 设置边框样式// 创建一个g元素用于放置内容const g = svgLen.append('g');// 复制主SVG中的内容到辅助SVGg.html(d3.select(svgMainRef).select('g').html());// 初始化辅助SVG的视图updateLenSVG(d3.zoomIdentity);};// 更新辅助SVG的缩放函数function updateLenSVG(transform) {const svgLen = d3.select(svgLenRef); // 选择辅助SVGconst g = svgLen.select('g'); // 选择g元素// 获取主SVG的尺寸const width = +d3.select(svgMainRef).attr('width');const height = +d3.select(svgMainRef).attr('height');// 计算放大区域的边界const centerX = width / 2; // 计算中心X坐标const centerY = height / 2; // 计算中心Y坐标const rectX = (centerX - 250) / transform.k - transform.x / transform.k; // 计算边界Xconst rectY = (centerY - 200) / transform.k - transform.y / transform.k; // 计算边界Y// 设置辅助SVG的视图范围g.attr('transform', `translate(${-rectX * transform.k}, ${-rectY * transform.k}) scale(${transform.k})`);// 更新辅助SVG中的内容g.html(d3.select(svgMainRef).select('g').html());// 重新计算X和Y的最小最大值const axisXOffset = 10; // X轴偏移const axisYOffset = 50; // Y轴偏移const xMin = Math.min((d3.min(xpoints(), d => d.coords[0])) - axisXOffset,(d3.min(ypoints(), d => d.coords[0])) - axisXOffset);const xMax = Math.max((d3.max(xpoints(), d => d.coords[0])) + axisXOffset,(d3.min(ypoints(), d => d.coords[0])) + axisXOffset);const yMin = Math.min((d3.min(xpoints(), d => d.coords[1])) - axisYOffset,(d3.min(ypoints(), d => d.coords[1])) - axisYOffset);const yMax = Math.max((d3.max(xpoints(), d => d.coords[1])) + axisYOffset,(d3.min(ypoints(), d => d.coords[1])) + axisYOffset);// 创建新的比例尺const x = d3.scaleLinear().domain([xMin, xMax]) // 输入范围.range([0, width - 80]); // 输出范围const y = d3.scaleLinear().domain([yMin, yMax]) // 输入范围.range([height - 50, 0]); // 输出范围// 更新Y坐标点的圆圈位置g.selectAll('circle.drawingData').data(ypoints()) // 绑定数据.attr('cx', d => x(d.coords[0])) // 更新X坐标.attr('cy', d => y(d.coords[1])); // 更新Y坐标// 移除旧的文本标签g.selectAll('text.drawingData').remove();// 重新绘制文本标签g.selectAll('circle.drawingData').each(function(d) {const circle = d3.select(this);g.append('text').attr('class','drawingData').attr('x', circle.attr('cx')).attr('y', parseFloat(circle.attr('cy')) - 10).text(d.text).attr('font-family', 'sans-serif').attr('font-size', '10px').attr('fill', 'red').attr('text-anchor', 'middle');});}// 返回包含两个SVG图形的JSX元素return (<div style={{ display: 'flex' }}><svg ref={svgMainRef}></svg> {/* 主SVG */}<svg ref={svgLenRef}></svg> {/* 辅助SVG */}</div>);
}export default Temp; // 导出Temp组件