浙江网站建设网关键词搜索排名
d3.js研发两组比较的分面柱状图
可调整分面在图的上方还是下方,动态计算位置
下面是项目里封装好的方法 可以直接调用
import * as d3 from "d3";const CMCTwoGroup = (options = {}) => {let originContainer = document.querySelector("#chart-container"),originHeight = originContainer.offsetHeight,originWidth = originContainer.offsetWidth;let height = originHeight * (options.params.height / 100);let width = originWidth * (options.params.width / 100);const container = d3.select(options.container);let svg = container.select("svg");// 获取标签样式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};}const rawData = options.data;// 数据预处理const divisions = Array.from(new Set(rawData.map((d) => d.Matrisome_Division)) // 去重).sort((a, b) => {// 按原始数据出现顺序排序return (rawData.findIndex((d) => d.Matrisome_Division === a) -rawData.findIndex((d) => d.Matrisome_Division === b));});const groupedData = d3.group(rawData, (d) => d.Matrisome_Division);// 生成分类顺序(保持大分类顺序)const orderedCategories = [];divisions.forEach((division) => {const seen = new Set();rawData.forEach((d) => {if (d.Matrisome_Division === division &&!seen.has(d.Matrisome_Category)) {seen.add(d.Matrisome_Category);orderedCategories.push(d.Matrisome_Category);}});});// 构建处理后的数据结构const processedData = orderedCategories.map((category) => {const division = rawData.find((d) => d.Matrisome_Category === category).Matrisome_Division;return {category,division,Up:rawData.find((d) => d.Matrisome_Category === category && d.Regulation === "Up")?.Number || 0,Down:rawData.find((d) => d.Matrisome_Category === category && d.Regulation === "Down")?.Number || 0};});// 配置参数let {bar_witdh = 35,up_color = "#66c2a5",down_color = "#fc8d62",isNumber = true,numberSize = 10,division_color = "#f0f0f0",division_postion = "top",class_label_font = "Arial",class_label_size = 12,class_label_color = "#000000",x_title = "111",x_title_color = "#000",x_title_font = "Arial",x_title_size = 14,x_text_rotate = -45,x_text_color = "#000000",x_text_size = 14,x_text_font = "Arial",y_title = "222",y_title_color = "#000",y_title_font = "Arial",y_title_size = 14,y_text_color = "#000000",y_text_size = 14,y_text_font = "Arial",main_title = "333",main_title_color = "#000",main_title_font = "Arial",main_title_size = 14} = options.params;x_text_rotate = -x_text_rotate;const mainTitleH = main_title? getSvgTextStyle({text: main_title,fontSize: main_title_size,fontFamily: main_title_font}).height + 20: 0;const xTitleH = x_title? getSvgTextStyle({text: x_title,fontSize: x_title_size,fontFamily: x_title_font}).height + 20: 0;const xAxisH =getSvgBandAxisStyle({fontSize: x_text_size,fontFamily: x_text_font,rotate: x_text_rotate,domain: orderedCategories}).height + 20;const yTitleH = y_title? getSvgTextStyle({text: y_title,fontSize: y_title_size,fontFamily: y_title_font}).height + 20: 0;const yAxisW =getSvgBandAxisStyle({fontSize: y_text_size,fontFamily: y_text_font,domain: [0, d3.max(processedData, (d) => Math.max(d.Up, d.Down)) * 1.2],orient: "left"}).width + 20;let margin = {top: 50 + mainTitleH,right: 80,bottom: xAxisH + xTitleH + 100,left: yAxisW + yTitleH};// 创建SVG画布if (svg.empty()) {svg = container.append("svg");} else {svg.selectAll("*").remove();}svg.attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom);// 比例尺配置const xScale = d3.scaleBand().domain(orderedCategories).range([margin.left + yAxisW, width]).paddingInner(0).paddingOuter(0);const xSubgroup = d3.scaleBand().domain(["Up", "Down"]).range([xScale.bandwidth() / 2 - bar_witdh,xScale.bandwidth() / 2 + bar_witdh]).padding(0);const yMax = d3.max(processedData, (d) => Math.max(d.Up, d.Down)) * 1.2;const yScale = d3.scaleLinear().domain([0, yMax]).range([height, margin.top]).nice();// 颜色方案const color = d3.scaleOrdinal().domain(["Up", "Down"]).range([up_color, down_color]);const divisionColor = d3.scaleOrdinal().domain(divisions).range([division_color]);// 绘制柱状图svg.append("g").selectAll("g").data(processedData).join("g").attr("transform", (d) => `translate(${xScale(d.category)},0)`).selectAll("rect").data((d) =>["Up", "Down"].map((reg) => ({reg,value: d[reg],category: d.category}))).join("rect").attr("class", "bar").attr("x", (d) => xSubgroup(d.reg)).attr("y", (d) => yScale(d.value)).attr("width", bar_witdh).attr("height", (d) => height - yScale(d.value)).attr("fill", (d) => color(d.reg));// 添加数值标签if (isNumber) {svg.append("g").selectAll("text").data(processedData.flatMap((d) =>["Up", "Down"].map((reg) => ({category: d.category,value: d[reg],x: xScale(d.category) + xSubgroup(reg) + bar_witdh / 2,y: yScale(d[reg])})))).join("text").text((d) => d.value).attr("x", (d) => d.x).attr("y", (d) => d.y - 5) // 在柱子顶部上方5px.style("text-anchor", "middle").style("font-size", numberSize).style("fill", "#333");}// 主标题svg.append("text").attr("class", "main-title").attr("x", (width + margin.right) / 2).attr("y", division_postion == "bottom" ? 40 : 20).text(main_title).attr("text-anchor", "middle").attr("font-family", main_title_font).attr("font-size", main_title_size).attr("fill", main_title_color);// X轴标题svg.append("text").attr("class", "axis-title").attr("x", (width + margin.right) / 2).attr("y",division_postion == "bottom" ? height + xAxisH + 60 : height + xAxisH + 20).text(x_title).attr("text-anchor", "middle").attr("font-family", x_title_font).attr("font-size", x_title_size).attr("fill", x_title_color);// Y轴标题svg.append("text").attr("class", "axis-title").attr("transform", `rotate(-90)`).attr("x", -height / 2).attr("y", margin.left - 40).text(y_title).attr("text-anchor", "middle").attr("font-family", y_title_font).attr("font-size", y_title_size).attr("fill", y_title_color);// y坐标轴svg.append("g").attr("transform", `translate(${margin.left + yAxisW}, 0)`).call(d3.axisLeft(yScale)).append("text").attr("stroke", "#333").attr("stroke-width", 1.5).attr("fill", "none").attr("shape-rendering", "crispEdges");svg.selectAll(".tick text").attr("font-family", y_text_font).style("font-size", y_text_size).style("fill", y_text_color);// 大分类背景const divisionRanges = [];let currentDivision = null;let startCategory = null;processedData.forEach((d, i) => {if (d.division !== currentDivision) {if (currentDivision) {const end = xScale(processedData[i - 1].category) + xScale.bandwidth();divisionRanges.push({division: currentDivision,start: xScale(startCategory),end: end,width: end - xScale(startCategory)});}currentDivision = d.division;startCategory = d.category;}// 处理最后一个元素if (i === processedData.length - 1) {const end = xScale(d.category) + xScale.bandwidth();divisionRanges.push({division: currentDivision,start: xScale(startCategory),end: end,width: end - xScale(startCategory)});}});// 绘制背景块svg.selectAll(".division-block").data(divisionRanges).join("rect").attr("class", "division-block").attr("x", (d) => d.start).attr("width", (d) => d.width).attr("y", division_postion == "bottom" ? height : main_title ? 45 : 10).attr("height", 40).attr("fill", (d) => divisionColor(d.division)).attr("stroke", "#000000").attr("stroke-width", 1);// 添加分类标签svg.selectAll(".division-label").data(divisionRanges).join("text").text((d) => d.division).attr("x", (d) => d.start + d.width / 2).attr("y",division_postion == "bottom" ? height + 20 : main_title ? 65 : 30).style("dominant-baseline", "middle").attr("text-anchor", "middle").attr("font-family", class_label_font).style("font-size", class_label_size).style("fill", class_label_color);// 坐标轴const xAxis = d3.axisBottom(xScale).tickValues(orderedCategories) // 使用分类数据作为刻度.tickFormat((d) => d);// 绘制X轴svg.append("g").attr("class", "x-axis").attr("transform",`translate(0,${division_postion == "bottom" ? height + 40 : height})`) // 定位到图表底部.call(xAxis).call((g) => {g.selectAll(".tick text").attr("fill", x_text_color).attr("font-size", x_text_size).attr("font-family", x_text_font).each(function () {const text = d3.select(this);text.attr("tmpY", text.attr("dy") || "0.71em"); // 保存原始偏移量}).attr("dy", (d, i, nodes) => {const rotate = x_text_rotate;if (rotate > 70 && rotate <= 90) return "0.35em";if (rotate >= -90 && rotate < -70) return "0.4em";return d3.select(nodes[i]).attr("tmpY");}).attr("text-anchor",x_text_rotate ? (x_text_rotate > 0 ? "start" : "end") : "middle").attr("transform",(d, i, nodes) =>`rotate(${x_text_rotate} 0 ${d3.select(nodes[i]).attr("y")})`);});// 图例const legend = svg.append("g").attr("transform", `translate(${width + 40}, ${height / 2 - 20})`) // 右侧居中.selectAll("g").data(["Up", "Down"]).enter().append("g").attr("transform", (d, i) => `translate(0,${i * 25})`); // 增加行间距legend.append("rect").attr("width", 18).attr("height", 18).attr("fill", color);legend.append("text").attr("x", 24).attr("y", 9).attr("dy", "0.32em").style("font-size", "12px") // 添加字体大小.text((d) => d);
};export default CMCTwoGroup;
调用方法:
CMCTwoGroup({data: plots,params: chartParam,container: "#bar-container"});
再附赠一个研发初期没封装的html版的吧!可以直接运行!
<!DOCTYPE html>
<html><head> </head><body><div id="chart"></div></body>
</html>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>// 获取标签样式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};}const rawData = [{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "ECM Regulators",Regulation: "Down",Number: 4},{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "ECM-affiliated Proteins",Regulation: "Down",Number: 3},{Matrisome_Division: "Core matrisome",Matrisome_Category: "ECM Glycoproteins",Regulation: "Up",Number: 2},{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "ECM Regulators",Regulation: "Up",Number: 2},{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "Secreted Factors",Regulation: "Up",Number: 2},{Matrisome_Division: "Core matrisome",Matrisome_Category: "ECM Glycoproteins",Regulation: "Down",Number: 1},{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "ECM-affiliated Proteins",Regulation: "Up",Number: 1},{Matrisome_Division: "Matrisome-associated",Matrisome_Category: "Secreted Factors",Regulation: "Down",Number: 0}];// 数据预处理const divisions = Array.from(new Set(rawData.map((d) => d.Matrisome_Division)) // 去重).sort((a, b) => {// 按原始数据出现顺序排序return (rawData.findIndex((d) => d.Matrisome_Division === a) -rawData.findIndex((d) => d.Matrisome_Division === b));});const groupedData = d3.group(rawData, (d) => d.Matrisome_Division);// 生成分类顺序(保持大分类顺序)const orderedCategories = [];divisions.forEach((division) => {const seen = new Set();rawData.forEach((d) => {if (d.Matrisome_Division === division &&!seen.has(d.Matrisome_Category)) {seen.add(d.Matrisome_Category);orderedCategories.push(d.Matrisome_Category);}});});// 构建处理后的数据结构const processedData = orderedCategories.map((category) => {const division = rawData.find((d) => d.Matrisome_Category === category).Matrisome_Division;return {category,division,Up:rawData.find((d) => d.Matrisome_Category === category && d.Regulation === "Up")?.Number || 0,Down:rawData.find((d) => d.Matrisome_Category === category && d.Regulation === "Down")?.Number || 0};});// 配置参数let // {bar_witdh = 35,bar_up_color = "#66c2a5",bar_down_color = "#fc8d62",opacity = 0.5,isNumber = true,division_color = "#f0f0f0",division_postion = "bottom",class_label_font = "Arial",class_label_size = 12,class_label_color = "#000000",x_title = "111",x_title_color = "#000",x_title_font = "Arial",x_title_size = 14,x_text_rotate = -45,x_text_color = "#000000",x_text_size = 14,x_text_font = "Arial",y_title = "222",y_title_color = "#000",y_title_font = "Arial",y_title_size = 14,y_text_color = "#000000",y_text_size = 14,y_text_font = "Arial",main_title = "333",main_title_color = "#000",main_title_font = "Arial",main_title_size = 14;// } = options.params;const mainTitleH = main_title? getSvgTextStyle({text: main_title,fontSize: main_title_size,fontFamily: main_title_font}).height + 20: 0;const xTitleH = x_title? getSvgTextStyle({text: x_title,fontSize: x_title_size,fontFamily: x_title_font}).height + 20: 0;const xAxisH =getSvgBandAxisStyle({fontSize: x_text_size,fontFamily: x_text_font,rotate: x_text_rotate,domain: orderedCategories}).height + 20;const yTitleH = y_title? getSvgTextStyle({text: y_title,fontSize: y_title_size,fontFamily: y_title_font}).height + 20: 0;const yAxisW =getSvgBandAxisStyle({fontSize: y_text_size,fontFamily: y_text_font,domain: [0, d3.max(processedData, (d) => Math.max(d.Up, d.Down)) * 1.2],orient: "left"}).width + 20;const margin = {top: 40 + mainTitleH,right: 150,bottom: xAxisH + xTitleH + 100,left: yAxisW + yTitleH};const width = 600;const height = 400;const svg = d3.select("#chart").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).append("g").attr("transform", `translate(${margin.left},${margin.top})`);// 比例尺配置const xScale = d3.scaleBand().domain(orderedCategories).range([0, width]).paddingInner(0).paddingOuter(0);const xSubgroup = d3.scaleBand().domain(["Up", "Down"]).range([xScale.bandwidth() / 2 - bar_witdh,xScale.bandwidth() / 2 + bar_witdh]).padding(0);const yMax = d3.max(processedData, (d) => Math.max(d.Up, d.Down)) * 1.2;const yScale = d3.scaleLinear().domain([0, yMax]).range([height, 0]).nice();// 颜色方案const color = d3.scaleOrdinal().domain(["Up", "Down"]).range([bar_up_color, bar_down_color]);const divisionColor = d3.scaleOrdinal().domain(divisions).range([division_color]);// 绘制柱状图svg.append("g").selectAll("g").data(processedData).join("g").attr("transform", (d) => `translate(${xScale(d.category)},0)`).selectAll("rect").data((d) =>["Up", "Down"].map((reg) => ({reg,value: d[reg],category: d.category}))).join("rect").attr("class", "bar").attr("x", (d) => xSubgroup(d.reg)).attr("y", (d) => yScale(d.value)).attr("width", bar_witdh).attr("height", (d) => height - yScale(d.value)).attr("fill", (d) => color(d.reg)).attr("fill-opacity", opacity);// 添加数值标签if (isNumber) {svg.append("g").selectAll("text").data(processedData.flatMap((d) =>["Up", "Down"].map((reg) => ({category: d.category,value: d[reg],x: xScale(d.category) + xSubgroup(reg) + bar_witdh / 2,y: yScale(d[reg])})))).join("text").text((d) => d.value).attr("x", (d) => d.x).attr("y", (d) => d.y - 5) // 在柱子顶部上方5px.style("text-anchor", "middle").style("font-size", "10px").style("fill", "#333");}// 主标题svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", -40).text(main_title).attr("text-anchor", "middle").attr("font-family", main_title_font).attr("font-size", main_title_size).attr("fill", main_title_color);// X轴标题svg.append("text").attr("class", "axis-title").attr("x", width / 2).attr("y",division_postion == "bottom" ? height + xAxisH + 60 : height + xAxisH + 20).text(x_title).attr("text-anchor", "middle").attr("font-family", x_title_font).attr("font-size", x_title_size).attr("fill", x_title_color);// Y轴标题svg.append("text").attr("class", "axis-title").attr("transform", `rotate(-90)`).attr("x", -height / 2).attr("y", -50).text(y_title).attr("text-anchor", "middle").attr("font-family", y_title_font).attr("font-size", y_title_size).attr("fill", y_title_color);// y轴svg.append("g").call(d3.axisLeft(yScale)).append("text").attr("fill", "#000").attr("transform", "rotate(-90)").attr("y", 6).attr("dy", "0.71em").attr("text-anchor", "end");// 坐标轴svg.append("g").call(d3.axisLeft(yScale)).append("text").attr("stroke", "#333").attr("stroke-width", 1.5).attr("fill", "none").attr("shape-rendering", "crispEdges").selectAll(".tick text").attr("font-family", y_text_font).style("font-size", y_text_size).style("fill", y_text_color);// 大分类背景const divisionRanges = [];let currentDivision = null;let startCategory = null;processedData.forEach((d, i) => {if (d.division !== currentDivision) {if (currentDivision) {const end = xScale(processedData[i - 1].category) + xScale.bandwidth();divisionRanges.push({division: currentDivision,start: xScale(startCategory),end: end,width: end - xScale(startCategory)});}currentDivision = d.division;startCategory = d.category;}// 处理最后一个元素if (i === processedData.length - 1) {const end = xScale(d.category) + xScale.bandwidth();divisionRanges.push({division: currentDivision,start: xScale(startCategory),end: end,width: end - xScale(startCategory)});}});// 绘制背景块svg.selectAll(".division-block").data(divisionRanges).join("rect").attr("class", "division-block").attr("x", (d) => d.start).attr("width", (d) => d.width).attr("y", division_postion == "bottom" ? height : 0).attr("height", 40).attr("fill", (d) => divisionColor(d.division)).attr("stroke", "#000000").attr("stroke-width", 1);// 添加分类标签svg.selectAll(".division-label").data(divisionRanges).join("text").text((d) => d.division).attr("x", (d) => d.start + d.width / 2).attr("y", division_postion == "bottom" ? height + 20 : 20).style("dominant-baseline", "middle").attr("text-anchor", "middle").attr("font-family", class_label_font).style("font-size", class_label_size).style("fill", class_label_color);// 坐标轴// 创建X轴生成器const xAxis = d3.axisBottom(xScale).tickValues(orderedCategories) // 使用分类数据作为刻度.tickFormat((d) => d); // 分类轴不需要数字格式化// 绘制X轴svg.append("g").attr("class", "x-axis").attr("transform",`translate(0,${division_postion == "bottom" ? height + 40 : height})`) // 定位到图表底部.call(xAxis).call((g) => {g.selectAll(".tick text").attr("fill", x_text_color).attr("font-size", x_text_size).attr("font-family", x_text_font).each(function () {const text = d3.select(this);text.attr("tmpY", text.attr("dy") || "0.71em"); // 保存原始偏移量}).attr("dy", (d, i, nodes) => {const rotate = x_text_rotate;if (rotate > 70 && rotate <= 90) return "0.35em";if (rotate >= -90 && rotate < -70) return "0.4em";return d3.select(nodes[i]).attr("tmpY");}).attr("text-anchor",x_text_rotate ? (x_text_rotate > 0 ? "start" : "end") : "middle").attr("transform",(d, i, nodes) =>`rotate(${x_text_rotate} 0 ${d3.select(nodes[i]).attr("y")})`);});// 图例const legend = svg.append("g").attr("transform", `translate(${width + 20}, ${height / 2 - 20})`) // 右侧居中.selectAll("g").data(["Up", "Down"]).enter().append("g").attr("transform", (d, i) => `translate(0,${i * 25})`); // 增加行间距legend.append("rect").attr("width", 18).attr("height", 18).attr("fill", color);legend.append("text").attr("x", 24).attr("y", 9).attr("dy", "0.32em").style("font-size", "12px") // 添加字体大小.text((d) => d);
</script>
HTML效果图:
完毕完毕!