数据要求 必须是N*N,即XY轴数量以及数据必须完全一致
下面是html页面研发的 如果需要封装可以直接将script部分拿出来 xy标题标签字体颜色 方格大小数字显隐等都可以传值控制;
下面是html代码 可以直接拿走右键运行看到效果图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>4.1</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<svg width="600" height="500"></svg>
<script>
const plot_data = {
matrix: [
[
"sample",
"A3",
"A2",
"A1",
"A4",
"C2",
"C3",
"C1",
"C4",
"B1",
"B2",
"B3",
"B4"
],
[
"Faecalibaculum",
1.2454201087092798,
1.1964599688062982,
1.2246279355528582,
1.2496145335411446,
-1.1306832991129674,
88,
-1.1596808814905255,
99,
-0.12124059145756938,
-0.10143316578527155,
-0.12243590396671605,
-0.14983416759172946
],
[
"Bacteroides",
1.2605676783253026,
1.339727208620543,
1.2829076474685954,
77,
-0.23170694303040953,
-0.22794263012723878,
-0.21307882520259627,
-0.2305369335977242,
-1.0474900342201063,
-1.0426278181600128,
-1.0452839536303442,
-1.0351560540660434
],
[
"Cellulophaga",
1.5851433795025447,
1.074970812194983,
88,
1.3904893035654822,
-0.304028853426327,
-0.33541785723770695,
-0.42347651429807537,
-0.39732304180540773,
-0.9514871367120481,
-0.9043794729438239,
-0.941333812654171,
-0.9514313959231242
],
[
"Akkermansia",
1.3225949238557109,
1.368432590547057,
1.3144745952844128,
99,
-0.8782222309203529,
-0.874643823788357,
-0.8747870824032014,
-0.8786077775723006,
-0.45807777042215875,
-0.45841195826897524,
-0.4596734943761438,
-0.44500903889116117
],
[
"Nonlabens",
1.363808613441155,
1.3847613250254138,
1.3780290886888504,
1.2689644461849265,
-0.5843329174362808,
-0.5836076326356598,
-0.5609224649685922,
-0.5847146173759441,
90,
-0.7704964602309672,
-0.7704964602309672,
-0.7704964602309672
],
[
"Blautia",
1.4246093983123997,
1.3686378934537253,
1.365796150259885,
1.2160031133875941,
-0.5180275395251902,
-0.5008148507877613,
-0.4887590499819987,
-0.8034941783271784,
-0.7893022080449602,
-0.7573943595229189,
99,
-0.7276051677318671
],
[
"Phocaeicola",
1.0062914192585513,
1.5768647964480265,
1.3954344630454159,
1.3841428489092116,
-0.621310052343907,
-0.6406103415387611,
-0.7193391664188062,
79,
-0.7193391664188062,
-0.7193391664188062,
-0.7193391664188062,
-0.7193391664188062
],
[
"Maribacter",
1.2082562341007996,
1.1971510370393013,
1.5508257240822152,
1.4318593548656116,
-0.6936548836947554,
-0.5325081642246413,
79,
-0.6936548836947554,
-0.6936548836947554,
-0.6936548836947554,
-0.6936548836947554,
-0.6936548836947554
],
[
"Anaerotignum",
1.338744560804633,
1.4070616195506538,
1.1469624299147148,
1.506239335144494,
-0.6748759931768119,
-0.6748759931768119,
-0.6748759931768119,
89,
-0.6748759931768119,
-0.6748759931768119,
-0.6748759931768119,
-0.6748759931768119
],
[
"Sphingobacterium",
1.1709023258926226,
1.3434498983751746,
1.231020601554567,
1.6386677028848644,
-0.6730050660884036,
-0.6730050660884036,
-0.6730050660884036,
-0.6730050660884036,
98,
-0.6730050660884036,
-0.6730050660884036,
-0.6730050660884036
],
[
"Enterococcus",
-0.6818033249663342,
-0.6818033249663342,
-0.6818033249663342,
-0.6818033249663342,
0.8014766051245957,
98,
-0.5267130341496897,
-0.6818033249663342,
-0.6818033249663342,
1.6986158574231869,
1.5069516179315536,
1.292292228434693
],
[
"Prevotella",
-0.4687733222910701,
-0.5199902535353341,
-0.3824132174608811,
78,
1.4647772102948815,
1.4385078048046929,
1.2352943704776311,
1.1922431111612675,
-0.8620962243211141,
-0.8428978132120464,
-0.8283283915285544,
-0.862049938058113
]
],
group: [
{
group: "A",
data: [
"Faecalibaculum",
"Bacteroides",
"Cellulophaga",
"Akkermansia"
]
},
{
group: "B",
data: ["Nonlabens", "Blautia", "Phocaeicola", "Maribacter"]
},
{
group: "C",
data: [
"Anaerotignum",
"Sphingobacterium",
"Enterococcus",
"Prevotella"
]
}
]
};
// 获取标签样式
function getSvgTextStyle({
text = "",
fontSize = 14,
fontFamily = "Arial",
fontWeight = "normal"
} = {}) {
const svg = d3
.select("body")
.append("svg")
.attr("class", "get-svg-text-style");
const textStyle = svg
.append("text")
.text(text)
.attr("font-size", fontSize)
.attr("font-family", fontFamily)
.attr("font-weight", fontWeight)
.node()
.getBBox();
svg.remove();
return {
width: textStyle.width,
height: textStyle.height
};
}
// 获取离散坐标轴宽高
function getSvgBandAxisStyle({
fontSize = 20,
orient = "bottom",
fontFamily = "Arial",
fontWeight = "normal",
rotate = 0,
domain = ["A", "B", "C"],
range = [0, 200]
} = {}) {
let axis;
let svg = d3
.select("body")
.append("svg")
.attr("width", 200)
.attr("height", 100)
.attr("transform", "translate(300, 200)")
.attr("class", "get-svg-axis-style");
let scale = d3.scaleBand().domain(domain).range(range);
if (orient === "bottom" || orient === "top") {
axis = d3.axisBottom(scale);
} else {
axis = d3.axisLeft(scale);
}
let axisStyle = svg
.append("g")
.call(axis)
.call((g) => {
g.selectAll("text")
.attr("fill", "#555")
.attr("font-size", fontSize)
.attr("font-family", fontFamily)
.attr("font-weight", fontWeight)
.attr(
"tmpY",
g.select("text").attr("tmpY") || g.select("text").attr("dy")
)
.attr(
"dy",
rotate > 70 && rotate <= 90
? "0.35em"
: rotate >= -90 && rotate < -70
? "0.4em"
: g.select("text").attr("tmpY")
)
.attr(
"text-anchor",
orient === "left"
? "end"
: rotate
? rotate > 0
? "start"
: "end"
: "middle"
)
.attr(
"transform",
`translate(0, 0) ${
rotate
? `rotate(${rotate} 0 ${g.select("text").attr("y")})`
: ""
}`
);
})
.node()
.getBBox();
svg.remove();
return {
width: axisStyle.width,
height: axisStyle.height
};
}
let colors = [
"#FF735A",
"#5BCCB6",
"#4782B3",
"#EB7BC0",
"#FFAA33",
"#FED840",
"#AB80F5",
"#EACC93",
"#5C9966",
"#A0C896",
"#EB8D8E",
"#CEAFAF"
],
main_title = "FastANI ANI values(clustered by ANI)",
main_title_color = "#000",
main_title_font = "Arial",
main_title_size = 14,
x_title = "xxxxxxx",
x_title_color = "#000",
x_title_font = "Arial",
x_title_size = 14,
x_text_color = "#000",
x_text_font = "Arial",
x_text_size = 14,
x_text_rotate = -60,
y_title = "yyyyyyy",
y_title_color = "#000",
y_title_font = "Arial",
y_title_size = 14,
y_text_color = "#000",
y_text_font = "Arial",
y_text_size = 14,
y_text_rotate = 0,
y_text_style = "normal",
legend_title = "legend title",
legend_title_color = "#000000",
legend_title_size = 14,
legend_title_font = "Arial",
legend_text_color = "#000000",
legend_text_size = 12,
legend_text_font = "Arial",
numVisable = true,
numSize = 8,
cellSize = 25,
littleColor = "pink";
const speciesInfo = plot_data.matrix.slice(1).map((row) => row[0]);
const heatmapData = plot_data.matrix.slice(1).map((row) => row.slice(1));
// 主标题宽高
const mainTitleW = getSvgTextStyle({
text: main_title,
fontSize: main_title_size,
fontFamily: main_title_font
}).width;
const mainTitleH = getSvgTextStyle({
text: main_title,
fontSize: main_title_size,
fontFamily: main_title_font
}).height;
// X轴及标题高度
const xTitleH = getSvgTextStyle({
text: x_title,
fontSize: x_title_size,
fontFamily: x_title_font
}).height;
const xTitleW = getSvgTextStyle({
text: x_title,
fontSize: x_title_size,
fontFamily: x_title_font
}).width;
const xAxisH = getSvgBandAxisStyle({
fontSize: x_text_size,
fontFamily: x_text_font,
rotate: x_text_rotate,
domain: speciesInfo
}).height;
// Y轴及标题高度
const yTitleH = getSvgTextStyle({
text: y_title,
fontSize: y_title_size,
fontFamily: y_title_font
}).height;
const yTitleW = getSvgTextStyle({
text: y_title,
fontSize: y_title_size,
fontFamily: y_title_font
}).width;
const yAxisW = getSvgBandAxisStyle({
fontSize: y_text_size,
fontFamily: y_text_font,
domain: speciesInfo,
orient: "left"
}).width;
const yAxisH = getSvgBandAxisStyle({
fontSize: y_text_size,
fontFamily: y_text_font,
domain: speciesInfo
}).height;
// 图例
const legendTitleW = getSvgTextStyle({
text: legend_title,
fontSize: legend_title_size,
fontFamily: legend_title_font
}).width;
const legendTitleH = getSvgTextStyle({
text: legend_title,
fontSize: legend_title_size,
fontFamily: legend_title_font
}).height;
const N = heatmapData.length;
let allRects = [];
heatmapData.forEach((row, rowIndex) => {
row.forEach((value, colIndex) => {
allRects.push({
value: heatmapData[rowIndex][colIndex],
rowIndex,
colIndex
});
});
});
const margin = { top: 30, right: 30, bottom: 30, left: 30 };
const width =
margin.left +
margin.right +
yTitleH +
yAxisW +
cellSize * N +
legendTitleH +
100;
const height =
margin.top +
margin.bottom +
mainTitleH +
cellSize * N +
xAxisH +
xTitleH;
100;
const svg = d3.select("svg").attr("width", width).attr("height", height);
const chartWidth = cellSize * N;
const chartHeight = cellSize * N;
// 缩放和轴
const xScale = d3
.scaleBand()
.domain(d3.range(heatmapData[0].length))
.range([0, chartWidth])
.padding(0.02);
const yScale = d3
.scaleBand()
.domain(d3.range(heatmapData.length))
.range([chartHeight, 0])
.padding(0.02);
const xAxis = d3.axisBottom(xScale).tickFormat((i) => speciesInfo[i]);
const yAxis = d3.axisLeft(yScale).tickFormat((i) => speciesInfo[i]);
// 创建一个根据索引映射到颜色数组的比例尺
const colorScale = d3
.scaleOrdinal()
.domain(d3.range(colors.length))
.range(colors);
// 绘制X、Y轴
svg
.append("g")
.attr("class", "axis x-axis")
.attr(
"transform",
`translate(${margin.left + yAxisW + yTitleH + 20}, ${
margin.top + mainTitleH + chartHeight + 10
})`
)
.call(xAxis)
.call((g) => {
g.selectAll("text")
.attr("fill", x_text_color)
.attr("font-size", x_text_size)
.attr("font-family", x_text_font)
.attr(
"tmpY",
g.select("text").attr("tmpY") || g.select("text").attr("dy")
)
.attr(
"dy",
x_text_rotate > 70 && x_text_rotate <= 90
? "0.35em"
: x_text_rotate >= -90 && x_text_rotate < -70
? "0.4em"
: g.select("text").attr("tmpY")
)
.attr(
"text-anchor",
x_text_rotate ? (x_text_rotate > 0 ? "start" : "end") : "middle"
)
.attr(
"transform",
`translate(0, 0) ${
x_text_rotate
? `rotate(${x_text_rotate} 0 ${g.select("text").attr("y")})`
: ""
}`
);
});
svg
.append("g")
.attr("class", "axis y-axis")
.attr(
"transform",
`translate(${margin.left + yAxisW + yTitleH}, ${
margin.top + mainTitleH - 10
})`
)
.call(yAxis)
.selectAll(".tick text")
.attr("fill", y_text_color)
.attr("font-size", y_text_size)
.attr("font-family", y_text_font);
// 绘制热图
const heatmapGroup = svg
.append("g")
.attr(
"transform",
`translate(${margin.left + yAxisW + yTitleH + 20},${
margin.top + mainTitleH - 10
})`
);
heatmapGroup
.selectAll("rect")
.data(allRects)
.enter()
.append("rect")
.attr("x", (d) => xScale(d.colIndex))
.attr("y", (d) => yScale(d.rowIndex))
.attr("width", cellSize)
.attr("height", cellSize)
.attr("rx", 5) // 设置圆角半径
.attr("ry", 5)
.style("fill", (d) => {
if (d.value <= 70) {
return littleColor;
} else {
return colorScale(Math.floor(d.value * colors.length));
}
})
.style("cursor", "pointer")
.on("mouseover", function (d, i) {
const colIndex = i.colIndex;
const rowIndex = i.rowIndex;
const yLabel = speciesInfo[rowIndex];
const xLabel = speciesInfo[colIndex];
heatmapGroup.selectAll("rect").filter(function (e, j) {
return !(
j % heatmapData[0].length === colIndex &&
Math.floor(j / heatmapData[0].length) === rowIndex
);
});
// .style("opacity", 0.5);
// 显示提示框
let tooltip = d3.select("body").select(".scatter-tooltip");
if (tooltip.empty()) {
// 如果提示框不存在,则创建它
tooltip = d3
.select("body")
.append("div")
.attr("class", "scatter-tooltip")
.style("position", "absolute")
.style("pointer-events", "none")
.style("opacity", 0);
}
// 更新提示框的内容和位置
tooltip
.html(
`<div>X:${xLabel}</br>
Y:${yLabel}</br>
值: ${i.value}</div>`
)
.style("left", `${event.pageX + 10}px`)
.style("top", `${event.pageY + 10}px`)
.style("padding", "7px 5px")
.style("border-radius", "4px")
.style("font-size", "12px")
.style("color", "#555")
.style("background", "rgba(255, 255, 255, .8)")
.style("border", `1px solid rgba(0, 0, 0, .5)`)
.style("opacity", 1);
d3.select(this).style("opacity", 1);
})
.on("mouseout", function () {
// 恢复所有矩形的透明度
// heatmapGroup.selectAll("rect").style("opacity", 1);
// 隐藏提示
d3.select(".scatter-tooltip").remove();
});
// 绘制数字标签
if (numVisable) {
heatmapGroup
.selectAll("text.text-label")
.data(allRects)
.enter()
.append("text")
.attr("class", "text-label")
.attr("font-size", numSize)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("x", (d) => xScale(d.colIndex) + xScale.bandwidth() / 2)
.attr("y", (d) => yScale(d.rowIndex) + yScale.bandwidth() / 2)
.text((d) => {
return d.value <= 70 ? "≤70" : d.value.toFixed(2);
});
}
// 条状物种信息
let groupColors = {};
// 遍历group数组
plot_data.group.forEach((groupItem, index) => {
if (index < colors.length) {
groupColors[groupItem.group] = colors[index];
} else {
groupColors[groupItem.group] = colors[index % colors.length];
}
});
// 辅助函数:根据物种名返回分组名
function getGroupForSpecies(species) {
for (let grp of plot_data.group) {
if (grp.data.includes(species)) {
return grp.group;
}
}
// 如果物种不在任何分组中,可以返回一个默认值或抛出错误
return null;
}
// 绘制条形图
// 绘制X轴条形图
const xAxisBars = svg
.append("g")
.attr("class", "x-axis-bars")
.attr(
"transform",
`translate(${margin.left + yAxisW + yTitleH + 20}, ${
margin.top + mainTitleH + chartHeight - 5
})`
);
// 遍历物种信息
speciesInfo.forEach((species, index) => {
// 获取物种所属分组的颜色
const groupIndex = getGroupForSpecies(species);
const color = groupIndex ? groupColors[groupIndex] : "#ccc"; // 如果没有找到分组,则使用默认颜色
// 绘制条形图
xAxisBars
.append("rect")
.attr("x", xScale(index))
.attr("y", 0)
.attr("width", xScale.bandwidth())
.attr("height", 8)
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", color);
});
// 创建一个用于绘制y轴条形的g元素
const yAxisBars = svg
.append("g")
.attr("class", "y-axis-bars")
.attr(
"transform",
`translate(${margin.left + yAxisW + yTitleH + 7}, ${
margin.top + mainTitleH - 10
})`
);
// 遍历物种信息
speciesInfo.forEach((species, index) => {
const groupIndex = getGroupForSpecies(species);
const color = groupIndex ? groupColors[groupIndex] : "#ccc"; // 如果没有找到分组,则使用默认颜色
// 绘制条形
yAxisBars
.append("rect")
.attr("x", 0)
.attr("y", yScale(index))
.attr("width", 8) // 条形的宽度
.attr("height", yScale.bandwidth())
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", color);
});
// 添加主标题
svg
.append("g")
.attr(
"transform",
`translate(${chartWidth / 2 + yAxisW + yTitleH + margin.left},${
margin.top - 5
})`
)
.append("text")
.attr("text-anchor", "middle")
.text(main_title)
.attr("fill", main_title_color)
.attr("font-size", main_title_size)
.attr("font-family", main_title_font);
// X轴标题
svg
.append("g")
.attr("class", "svg-x-title")
.append("text")
.text(x_title)
.attr("fill", x_title_color)
.attr("font-family", x_title_font)
.attr("font-size", x_title_size)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "hanging")
.attr(
"transform",
`translate(${chartWidth / 2 + yAxisW + yTitleH + margin.left}, ${
margin.top + mainTitleH + chartHeight + xAxisH + 20
})`
);
// Y轴标题
svg
.append("g")
.attr("class", "svg-y-title")
.append("text")
.text(y_title)
.attr("fill", y_title_color)
.attr("font-family", y_title_font)
.attr("font-size", y_title_size)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "hanging")
.attr(
"transform",
`translate(${margin.left - 10}, ${
margin.top + mainTitleH + chartHeight / 2
}) rotate(-90)`
);
// 图例
const legend_container = svg
.append("g")
.attr("class", "legend-container")
.attr(
"transform",
`translate(${margin.left + yTitleH + yAxisW + chartWidth + 40},${
margin.top + mainTitleH
})`
);
legend_container
.append("g")
.attr("class", "legend-title-container")
.append("text")
.attr("x", -5)
.attr("y", 5)
.attr("class", "legend-title")
.text(legend_title)
.attr("fill", legend_title_color)
.attr("font-family", legend_title_font)
.attr("font-size", legend_title_size);
const defs = legend_container.append("defs");
const linearGradient = defs
.append("linearGradient")
.attr("id", "linearColor")
.attr(
"transform",
`translate(${width - margin.right - legendTitleW},${
margin.top + mainTitleH + 20
})`
)
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
linearGradient
.append("stop")
.attr("offset", "0%")
.style("stop-color", colors[2]);
linearGradient
.append("stop")
.attr("offset", "50%")
.style("stop-color", colors[1]);
linearGradient
.append("stop")
.attr("offset", "100%")
.style("stop-color", colors[0]);
legend_container
.append("rect")
.attr("x", 0)
.attr("y", 20)
.attr("width", 10)
.attr("height", 70)
.style("fill", "url(#" + linearGradient.attr("id") + ")");
//颜色三位图例值
// const data_tip = [
// d3.min(allRects, (d) => {
// return Number(d.value);
// }),
// d3.median(allRects, (d) => {
// return Number(d.value);
// }),
// d3.max(allRects, (d) => {
// return Number(d.value);
// })
// ];
// const data_tip_backup = [];
// data_tip.map((item) => {
// data_tip_backup.push(Number(item).toFixed(2));
// });
//颜色五位四段图例值
const values = allRects.map((d) => Number(d.value));
values.sort((a, b) => a - b);
// 计算分位数的索引
const n = values.length;
const q1Index = Math.floor((n - 1) * 0.25); // 第一四分位数
const q3Index = Math.floor((n - 1) * 0.75); // 第三四分位数
// 使用d3.quantile(但这里我们实际上使用排序后的数组索引)或直接索引来获取值
const data_tip = [
d3.min(values), // 最小值
values[q1Index], // 第一四分位数
d3.median(values), // 中位数
values[q3Index], // 第三四分位数
d3.max(values) // 最大值
];
const data_tip_backup = data_tip.map((item) => item.toFixed(2));
legend_container
.append("g")
.selectAll(".ledend_text")
.data(data_tip_backup)
.enter()
.append("text")
.attr("class", "legend-text")
.text((d) => {
return d;
})
.attr("x", 15)
.attr("y", (_, i) => {
if (i === 0) {
return 90;
} else if (i === 1) {
return 75;
} else if (i === 2) {
return 60;
} else if (i === 3) {
return 44;
} else {
return 28;
}
})
.attr("text-anchor", "start")
.attr("fill", "#000000")
.attr("font-size", 11)
.attr("font-family", 11);
const group_container = legend_container
.append("g")
.attr("class", "group-container")
.attr("transform", `translate(0,110)`);
let groupArr = [];
plot_data.group.forEach((item) => {
groupArr.push(item.group);
});
group_container
.selectAll(".group-rect")
.data(groupArr)
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (_, i) => {
return i * 13;
})
.attr("width", 20)
.attr("height", 8)
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", (_, i) => colors[i % colors.length]);
group_container
.append("g")
.selectAll(".ledend_text")
.data(groupArr)
.enter()
.append("text")
.attr("class", "legend-text")
.text((d) => {
return d;
})
.attr("x", 27)
.attr("y", (_, i) => {
const legendOffsetY = 7;
return i * 14 + legendOffsetY;
})
.attr("text-anchor", "start")
.attr("fill", legend_text_color)
.attr("font-size", legend_text_size)
.attr("font-family", legend_text_font);
</script>
</body>
</html>
svg画布的宽高是根据数据量以及画的图的大小来算的,不用担心哪部分位置不够这些问题;plot_data是我自己拟的假数据 并在画图时做了处理,如数据格式不同,处理下数据,画图方法基本可以不用改动;
颜色的图例这里是分了五位四段 可以找到图例分段的代码自行修改;
左边和下面的条状是另外画的,表示分组;也是做了部分数据处理;
还有个处理是小于70的方格不显示具体数值只显示小于等于70,且颜色可自定义修改,不需要可以不处理 直接拿颜色数组渲染就行;自定义的颜色和大于70的颜色 都有变量控制,直接传值就可以了;
其他的暂时想不起来了 画图的时候基本都敲了注释 因为产品也不知道到底要做成什么样,只能是我画一点他们看一点才能提一点需求,加加改改的,忘了我就再看看!哈哈哈哈哈
展示效果图: