记录:d3Js实现高通量 GraPhlAn图

效果图:

一、对应传参的数据格式截图:


1、content(用于获取图的title、以及带过来当前分析需要的物种名List):

2、树形图数据resultData:

3、外圈数据outSideArcData(二维数组:大数组的长度代表外圈的个数,子数组的长度代表是由多少个物种):

 4、分组方案数据groupSchemeSelect(非必传)

二、这是自己整理出的js文件代码,若数据符合,可直接调用

import * as d3 from 'd3';
import { checkStr, getColor, cutString, getTextWidth, getStrLength } from '@/utils/utils';
import _ from 'lodash';

// 计算当前树的位置
const step = (startAngle, startRadius, endAngle, endRadius) => {
	const c0 = Math.cos((startAngle = ((startAngle - 90) / 180) * Math.PI));
	const s0 = Math.sin(startAngle);
	const c1 = Math.cos((endAngle = ((endAngle - 90) / 180) * Math.PI));
	const s1 = Math.sin(endAngle);

	return (
		'M' +
		startRadius * c0 +
		',' +
		startRadius * s0 +
		(endAngle === startAngle
			? ''
			: 'A' +
			  startRadius +
			  ',' +
			  startRadius +
			  ' 0 0 ' +
			  (endAngle > startAngle ? 1 : 0) +
			  ' ' +
			  startRadius * c1 +
			  ',' +
			  startRadius * s1) +
		'L' +
		endRadius * c1 +
		',' +
		endRadius * s1
	);
};

// 计算当前物种的文本长度
const fetchRoundTiltedHeight = (strArr, rotateAngle, fontStyle, radius) => {
	let longestStr = '';
	strArr.forEach(item => {
		if (getStrLength(item) > getStrLength(longestStr)) {
			longestStr = item;
		}
	});
	// 根据字体获取当前的文本宽度
	const curTextWidth = getTextWidth(longestStr, fontStyle) + radius;
	// 根据勾股定理获取当前旋转后所占的高度
	const curRotateTextWidth = Number((curTextWidth * Math.sin(rotateAngle))?.toFixed(2));

	return curRotateTextWidth - radius;
};

// 小矩形或小三角的颜色
const rectColor = (d, speciesData, averageAbounce, type, arr) => {
	const name =
		(d?.data?.analysisResults &&
			d.data.analysisResults[0] &&
			d.data.analysisResults[0]?.speciesName) ||
		'';
	const curData = speciesData.filter(item => item.speciesName === name);
	if (curData?.length) {
		arr && arr.push(curData);
		const rowData = _.cloneDeep(curData[0]);
		delete rowData.speciesName;
		let averageNum = 0;
		Object.values(rowData).forEach((item, index) => {
			averageNum += item;
			if (index + 1 === Object.values(rowData).length) {
				averageNum = averageNum / (index + 1);
			}
		});
		if (type === 'isRect') {
			if (averageNum >= averageAbounce) {
				return '#c501ba';
			}
		} else {
			if (averageNum < averageAbounce) {
				return '#bdc43c';
			}
		}

		return 'none';
	}
	return 'none';
};

// 分类水平层级表 用于link图有几级
const tax = ['kingdom', 'phylum', 'clazz', 'ord', 'family', 'genus', 'species', 'otu'];

export default function renderGraphlan(
	container, // 当前画图的作用域
	content,  // 当前画图的宽高已经其他一些需要画图的信息
	resultData, // 画生命树的基本数据(树形结构的json)
	outSideArcData, // 画外围圆圈的基本数据
	averageAbounce, // 平均丰度(number类型)
	groupSchemeSelect, // 分组方案
) {
	if (resultData?.length && outSideArcData?.length) {
		// 总丰度
		let totalLdaValue = 0;
		resultData.forEach(item => {
			totalLdaValue += item.ldaValue;
		});
		d3.select('div#' + container)
			.selectAll('*')
			.remove();
		const root = { children: resultData, name: '' };
		const {
			size: { width, height },
		} = content;
		const chartName = content.params && content.params.title;
		const speciesData =
			content.params &&
			content.params.tableResultData &&
			content.params.tableResultData.species;

		// 取出当前最长文本
		const strArr = speciesData.map(item => item.speciesName);
		const rotateAngle = Math.PI / 2;
		const fontStyle = '8px Microsoft YaHei';
		const roundTiltedMaxwidth = fetchRoundTiltedHeight(strArr, rotateAngle, fontStyle, 0);

		// 外圈的通用渲染数据
		const commonDataSet = [];
		for (let i = 0; i < outSideArcData[0]?.length; i++) {
			const whichPart = resultData.findIndex(item => i + 1 <= item.number);
			commonDataSet.push({
				index: i,
				startAngle: ((Math.PI * 2) / outSideArcData[0].length) * i,
				endAngle: ((Math.PI * 2) / outSideArcData[0].length) * (i + 1),
				whichPart,
			});
		}

		// 外圈的数据
		let curOutSideArcData = outSideArcData;
		if (groupSchemeSelect?.length) {
			const tempOutSideArcDataArr = [];
			groupSchemeSelect.forEach(item => {
				if (item?.groupsData?.length) {
					tempOutSideArcDataArr.push(item.groupsData[0]);
				}
			});
			curOutSideArcData = tempOutSideArcDataArr;
		}

		if (curOutSideArcData?.length > 10) {
			curOutSideArcData.splice(10, curOutSideArcData.length - 1);
		}

		// links最大半径
		const radius = 300;
		const treeRadius = radius - roundTiltedMaxwidth;

		// 一行展示图例个数
		const lengedNum = 5;
		// 图例所占的高度 宽度
		const lengedHeight = Math.ceil(curOutSideArcData.length / lengedNum) * 30;
		let lengedWidth = curOutSideArcData.length * 180;
		// 超过五个直接按照最大计算
		if (curOutSideArcData.length / lengedNum > 1) {
			lengedWidth = lengedNum * 180;
		}
		// 外圈所占的height
		const curOutSideArcDataHeight = curOutSideArcData.length * 20;
		const curOutSideArcDataWidth = curOutSideArcData.length * 20 + 640;
		// 最左边的边界值
		const leftBorderVal = 330 + curOutSideArcData.length * 10;
		// 最上边的边界值
		const topBorderVal = 400;
		// 标题距离图的间隙
		const gap = 20;
		// 动态的圆形位置
		const curTranslate = `translate(${leftBorderVal +
			(width - curOutSideArcDataWidth) / 2} ${topBorderVal +
			curOutSideArcDataHeight / 2 +
			gap})`;

		// 外圈样本数据集合(按照树图最外层物种顺序来排序的data)
		const newOutSideData = [];

		const tree = d3
			.tree()
			.size([360, treeRadius])
			.separation(function() {
				return 10;
			});

		// 画布所需增加的额外高度 (30为预留gap)
		const otherHeight = lengedHeight + curOutSideArcDataHeight + 30;

		const svg = d3
			.select('#' + container)
			.append('svg')
			.attr('width', width)
			.attr('height', height + otherHeight)
			.attr('version', 1.1)
			.attr('xmlns', 'http://www.w3.org/2000/svg');

		// 外边圆的最小半径
		const outerRadius = 330;

		// 内圈通用数据
		const insideArcData = [];
		resultData.forEach(item => {
			item.children.forEach((citem, cindex) => {
				insideArcData.push(citem);
			});
		});

		const colorList = getColor(insideArcData.length);
		const insideDataSet = [];
		let startPart = 0;
		let endPart = 0;
		insideArcData.forEach((item, index) => {
			if (index > 0) {
				startPart = endPart;
			} else {
				startPart = 0;
			}

			endPart += item.number;
			insideDataSet.push({
				index,
				startAngle: ((Math.PI * 2) / curOutSideArcData[0].length) * startPart,
				endAngle: ((Math.PI * 2) / curOutSideArcData[0].length) * endPart,
				whichPart: index,
			});
		});

		// links的空白位置圆 需与 底色圆内圈的半径相等
		const curIndex = tax.findIndex(item => item === resultData.taxonomy);
		// 从门的位置开始计算(内圈大小是截止到门),下标从0开始,需要加1;
		const phylumIndex = tax.findIndex(item => item === 'phylum') + 1;
		const curInnerRadius = (treeRadius / (curIndex + phylumIndex)) * phylumIndex;
		const insideArc = d3
			.arc()
			.innerRadius(curInnerRadius)
			.outerRadius(outerRadius - 20);

		const insideArcGroup = svg
			.append('g')
			.attr('transform', curTranslate)
			.selectAll('g')
			.data(insideDataSet)
			.join('g');

		insideArcGroup
			.append('path')
			.attr('fill', d => {
				return colorList[d.whichPart];
			})
			.attr('opacity', '0.3')
			.attr('d', insideArc);

		const treeRoot = d3.hierarchy(root);

		tree(treeRoot);

		// 基于此画出圆以及扇形背景色、小圆球、文本
		const nodes = treeRoot.descendants();
		// 基于此画出鱼叉状的线
		const links = treeRoot.links();

		const titleLeft = leftBorderVal + (width - curOutSideArcDataWidth) / 2;

		// 渲染标题
		svg.append('g')
			.attr('transform', `translate(${titleLeft} 28)`)
			.append('text')
			.text(chartName)
			.attr('font-size', '12px')
			.attr('font-family', function() {
				if (checkStr(chartName) === 1) {
					return 'PingFang SC, Microsoft YaHei';
				} else if (checkStr(chartName) === 2) {
					return 'Helveticah, Arial';
				} else {
					return 'Helveticah, Arial, PingFang SC, Microsoft YaHei';
				}
			})
			.attr('text-anchor', 'middle')
			.attr('y', 40)
			.attr('id', 'title')
			.attr('style', 'cursor: pointer')
			.call(function() {
				svg.selectAll('#title').on('dblclick', function() {
					content.params.titleDoubleClick();
				});
			});

		// 渲染物种名
		svg.append('g')
			.attr('transform', curTranslate)
			.selectAll('text')
			.data(treeRoot.leaves())
			.join('text')
			.attr('style', 'font-size:8px')
			.attr(
				'transform',
				d =>
					// 文字的偏移
					`rotate(${d.x - (d.x < 180 ? 89.5 : 90.5)}) translate(${310 -
						roundTiltedMaxwidth},0)${d.x < 180 ? '' : ' rotate(180)'}`,
			)
			.attr('text-anchor', d => (d.x < 180 ? 'start' : 'end'))
			.text(d => {
				const name =
					(d?.data?.analysisResults &&
						d.data.analysisResults[0] &&
						d.data.analysisResults[0]?.speciesName) ||
					'';
				return cutString(name, 60);
			})
			.on('mouseover', function(d) {
				const left = `${d3.event.layerX}px`;
				const top = `${d3.event.layerY}px`;
				const name =
					(d?.data?.analysisResults &&
						d.data.analysisResults[0] &&
						d.data.analysisResults[0]?.speciesName) ||
					'';
				// 鼠标悬浮文字提示
				d3.select('div#' + container)
					.append('div')
					.attr('class', 'tooltip')
					.style('background', '#fff')
					.style('color', '#000')
					.style('padding', '0 10px')
					.style('border-radius', '3px')
					.style('box-shadow', '4px 4px 3px #888888')
					.style('position', 'absolute')
					.style('z-index', '10')
					.style('visibility', 'visible')
					.style('left', left)
					.style('top', top)
					.text(name);
			})
			.on('mouseout', function() {
				d3.select('div#' + container)
					.selectAll('div.tooltip')
					.remove();
			});

		// 渲染矩形
		svg.append('g')
			.attr('transform', curTranslate)
			.selectAll('rect')
			.data(treeRoot.leaves())
			.join('rect') // 添加一个矩形
			.attr(
				'transform',
				d =>
					// 矩形的偏移
					`rotate(${d.x - 91.5}) translate(${outerRadius - 10},0)${'rotate(92)'}`,
			)
			.attr('x', 0)
			.attr('y', 0)
			.attr('width', 18)
			.attr('height', 6)
			.attr('fill', d => {
				return rectColor(d, speciesData, averageAbounce, 'isRect', newOutSideData);
			});

		// 渲染小三角
		svg.append('g')
			.attr('transform', curTranslate)
			.selectAll('path')
			.data(treeRoot.leaves())
			.join('path')
			.attr(
				'transform',
				d =>
					// 小三角的偏移
					`rotate(${d.x - 90}) translate(${outerRadius - 8},0)${'rotate(270)'}`,
			)
			.attr('fill', d => {
				return rectColor(d, speciesData, averageAbounce, 'noRect');
			})
			.attr('d', 'M0 0 L-10 8 L10 8 Z');

		// 渲染外边圆
		curOutSideArcData.forEach((item, index) => {
			// 外圈最多展示10个
			if (index > 9) return;
			const outArc = d3
				.arc()
				.innerRadius(outerRadius + 10 * index)
				.outerRadius(outerRadius + 10 * (index + 1));

			const averageValueItem = _.cloneDeep(item);

			// 取最大值 决定当前弧度的颜色深浅
			const valueArr = [];
			averageValueItem.forEach((averageItem, index) => {
				averageItem[0] = 24;
				valueArr.push(item[index][0]);
			});
			const maxValue = Math.max.apply(null, valueArr);

			svg.append('g')
				.attr('transform', curTranslate)
				.selectAll('g')
				.data(commonDataSet)
				.join('g')
				.append('path')
				.attr('fill', () => {
					return curOutSideArcData[index]?.color;
				})
				.attr('opacity', d => {
					let curVal = 0;
					if (
						newOutSideData &&
						newOutSideData[d.index] &&
						newOutSideData[d.index][0] &&
						newOutSideData[d.index][0][index]
					) {
						curVal = newOutSideData[d.index][0][index];
					}
					const alph = (curVal / maxValue < 0.1 ? 0.1 : curVal / maxValue).toFixed(1);
					return alph;
				})
				.attr('d', outArc);
		});

		// link图容器
		const chart = svg.append('g').attr('transform', curTranslate);

		// 画鱼叉形状的线
		chart
			.selectAll('.link')
			.data(links)
			.enter()
			.append('path')
			.attr('stroke', function(d) {
				// 鱼叉线条的颜色

				if (
					d?.source?.data?.partIndex >= 0 &&
					d?.source?.data?.id?.indexOf('kingdom') < 0
				) {
					return colorList[d.source.data.partIndex];
				}

				return '#000';
			})
			.attr('stroke-width', 1.0)
			.style('fill', 'none')
			.join('path')
			.attr('d', function(d) {
				return step(d.source.x, d.source.y, d.target.x, d.target.y);
			});

		// 空心圆容器
		const node = chart
			.append('g')
			.selectAll('.node')
			.data(nodes)
			.enter()
			.append('g')
			.attr('transform', function(d) {
				return `rotate(${d.x - 90}) translate(${d.y})`;
			});

		// 设置线和圆形
		node.append('circle')
			.attr('class', 'circleNode')
			.attr('stroke', 'grey')
			.attr('transform', () => {
				return `translate(-2)`;
			})
			.attr('r', function(d, index) {
				if (index) {
					const roundSize = Math.ceil(
						((d.data.ldaValue || d.parent.data.ldaValue) / totalLdaValue) * 4,
					);
					// 圆(相对丰度越大 圆越大)
					return roundSize;
				}
			})
			.style('fill', 'none');

		// 图例容器
		const lengedContianer = svg
			.append('g')
			.attr(
				'transform',
				`translate(${(width - lengedWidth) / 2} ${height +
					otherHeight -
					lengedHeight / 2})`,
			);
		// 渲染图例
		curOutSideArcData.forEach((item, index) => {
			const rowIndex = parseInt(index / lengedNum);
			const oneLenged = lengedContianer.append('g');
			oneLenged
				.append('rect')
				.attr('transform', `translate(${(index % lengedNum) * 180} ${18 * rowIndex})`)
				.attr('width', 24)
				.attr('height', 12)
				.attr('fill', item.color);
			oneLenged
				.append('text')
				.attr(
					'transform',
					`translate(${180 * (index % lengedNum) + 30} ${10 + rowIndex * 18})`,
				)
				.text(() => {
					return item.name;
				});
		});

		return otherHeight;
	}
}

  如有看不懂的api可参照d3官网:Tree of Life / D3 / Observable

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值