效果图:
一、对应传参的数据格式截图:
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