Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
modules/chart_engine/template/d3-js/type13_pictorial_percentage_bar_chart_01/pictorial_percentage_bar_plain_chart_01.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
REQUIREMENTS_BEGIN
|
| 3 |
+
{
|
| 4 |
+
"chart_type": "Pictorial Percentage Bar Chart",
|
| 5 |
+
"chart_name": "pictorial_percentage_bar_plain_chart_01",
|
| 6 |
+
"is_composite": false,
|
| 7 |
+
"required_fields": ["x", "y"],
|
| 8 |
+
"required_fields_type": [["categorical"], ["numerical"]],
|
| 9 |
+
"required_fields_range": [[2, 10], [0, "inf"]],
|
| 10 |
+
"required_fields_icons": ["x"],
|
| 11 |
+
"required_other_icons": [],
|
| 12 |
+
"required_fields_colors": [],
|
| 13 |
+
"required_other_colors": [],
|
| 14 |
+
"supported_effects": [],
|
| 15 |
+
"min_height": 400,
|
| 16 |
+
"min_width": 400,
|
| 17 |
+
"background": "no",
|
| 18 |
+
"icon_mark": "none",
|
| 19 |
+
"icon_label": "side",
|
| 20 |
+
"has_x_axis": "no",
|
| 21 |
+
"has_y_axis": "no"
|
| 22 |
+
}
|
| 23 |
+
REQUIREMENTS_END
|
| 24 |
+
*/
|
| 25 |
+
|
| 26 |
+
// 实现花瓶形状的柱状图 - 使用D3.js
|
| 27 |
+
function makeChart(containerSelector, data) {
|
| 28 |
+
const colorResolver = chartUtils.color.resolver(data);
|
| 29 |
+
// ---------- 1. 数据准备 ----------
|
| 30 |
+
// 提取数据和配置
|
| 31 |
+
const jsonData = data; // 完整的JSON数据对象
|
| 32 |
+
const chartData = jsonData.data.data || []; // 图表数据
|
| 33 |
+
const variables = jsonData.variables || {}; // 图表配置,如果不存在则使用空对象
|
| 34 |
+
const typography = jsonData.typography || { // 字体设置,如果不存在则使用默认值
|
| 35 |
+
title: { font_family: "Arial", font_size: "28px", font_weight: 700 },
|
| 36 |
+
label: { font_family: "Arial", font_size: "16px", font_weight: 500 },
|
| 37 |
+
description: { font_family: "Arial", font_size: "16px", font_weight: 500 },
|
| 38 |
+
annotation: { font_family: "Arial", font_size: "12px", font_weight: 400 }
|
| 39 |
+
};
|
| 40 |
+
const colors = jsonData.colors || {
|
| 41 |
+
text_color: "#000000",
|
| 42 |
+
other: { primary: "#4682B4", secondary: "#FF7F50" }
|
| 43 |
+
};
|
| 44 |
+
const images = jsonData.images || { field: {}, other: {} }; // 图像(国旗等)
|
| 45 |
+
const dataColumns = chartUtils.schema.columns(jsonData); // 使用data_columns
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
// 清空容器
|
| 49 |
+
d3.select(containerSelector).html("");
|
| 50 |
+
|
| 51 |
+
// ---------- 2. 提取字段名称 ----------
|
| 52 |
+
let xField, yField;
|
| 53 |
+
|
| 54 |
+
// 安全地提取字段名称
|
| 55 |
+
const xChannel = chartUtils.schema.channel(jsonData, "x");
|
| 56 |
+
const yChannel = chartUtils.schema.channel(jsonData, "y");
|
| 57 |
+
|
| 58 |
+
xField = xChannel.key;
|
| 59 |
+
yField = yChannel.key;
|
| 60 |
+
|
| 61 |
+
// 获取字段单位(如果存在)
|
| 62 |
+
let dimensionUnit = "";
|
| 63 |
+
let valueUnit = ""; // 默认为百分比
|
| 64 |
+
|
| 65 |
+
if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
|
| 66 |
+
dimensionUnit = chartUtils.schema.channel(jsonData, "x").unit;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
|
| 70 |
+
valueUnit = chartUtils.schema.channel(jsonData, "y").unit;
|
| 71 |
+
}
|
| 72 |
+
// 添加数值格式化函数
|
| 73 |
+
// ---------- 3. 数据处理 ----------
|
| 74 |
+
// 按照y值降序排序数据
|
| 75 |
+
const sortedData = [...chartData].sort((a, b) => b[yField] - a[yField]);
|
| 76 |
+
|
| 77 |
+
// ---------- 4. 尺寸和布局设置 ----------
|
| 78 |
+
const width = variables.width || 800;
|
| 79 |
+
const height = variables.height || 600;
|
| 80 |
+
|
| 81 |
+
// 边距: 上, 右, 下, 左
|
| 82 |
+
const margin = {
|
| 83 |
+
top: 120, // 减少顶部空间
|
| 84 |
+
right: 30,
|
| 85 |
+
bottom: 80, // 国旗和标签的空间
|
| 86 |
+
left: 30
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
// 计算实际绘图区域尺寸
|
| 90 |
+
const innerWidth = width - margin.left - margin.right;
|
| 91 |
+
const innerHeight = height - margin.top - margin.bottom;
|
| 92 |
+
|
| 93 |
+
// ---------- 5. 创建SVG容器 ----------
|
| 94 |
+
const svg = d3.select(containerSelector)
|
| 95 |
+
.append("svg")
|
| 96 |
+
.attr("width", width)
|
| 97 |
+
.attr("height", height)
|
| 98 |
+
.attr("viewBox", `0 0 ${width} ${height}`)
|
| 99 |
+
.attr("style", "max-width: 100%; height: auto;")
|
| 100 |
+
.attr("xmlns", "http://www.w3.org/2000/svg")
|
| 101 |
+
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
// 创建主图表组
|
| 105 |
+
const chart = svg.append("g")
|
| 106 |
+
.attr("transform", `translate(${margin.left}, ${margin.top})`);
|
| 107 |
+
|
| 108 |
+
// ---------- 6. 创建比例尺 ----------
|
| 109 |
+
// X轴比例尺(瓶子之间的间隔)
|
| 110 |
+
const xScale = d3.scaleBand()
|
| 111 |
+
.domain(sortedData.map(d => d[xField]))
|
| 112 |
+
.range([0, innerWidth])
|
| 113 |
+
.padding(0.2);
|
| 114 |
+
|
| 115 |
+
// Y轴比例尺(瓶子的高度)
|
| 116 |
+
const yMax = d3.max(sortedData, d => +d[yField]);
|
| 117 |
+
const yScale = d3.scaleLinear()
|
| 118 |
+
.domain([0, yMax * 1.1]) // 添加10%的空间
|
| 119 |
+
.range([innerHeight, 0]);
|
| 120 |
+
|
| 121 |
+
// ---------- 7. 创建瓶子和蒙版 ----------
|
| 122 |
+
// 定义瓶子的宽度和高度 (不修改这部分)
|
| 123 |
+
const bottleWidth = xScale.bandwidth();
|
| 124 |
+
const bottleHeight = Math.max(bottleWidth * 3, innerHeight * 0.5);
|
| 125 |
+
|
| 126 |
+
// 获取瓶子的主要颜色
|
| 127 |
+
const bottleColor = colorResolver.other("primary", { fallback: "#4CAF50" }).value;
|
| 128 |
+
|
| 129 |
+
// 定义国旗和文本的空间
|
| 130 |
+
const flagTextHeight = 50;
|
| 131 |
+
|
| 132 |
+
// 计算瓶子底部应在的位置 - 让瓶子底部接近国旗
|
| 133 |
+
const bottleBottomY = innerHeight - 10; // 瓶子底部位置,紧贴国旗上方
|
| 134 |
+
const bottleTopY = bottleBottomY - bottleHeight; // 瓶子顶部位置
|
| 135 |
+
|
| 136 |
+
// 创建一个数组来存储所有的值标签元素引用
|
| 137 |
+
const valueLabels = [];
|
| 138 |
+
|
| 139 |
+
const bottleImageSVG = `<svg viewBox="0 0 200 500" xmlns="http://www.w3.org/2000/svg">
|
| 140 |
+
<path d="M60,280 L60,450 Q100,480 140,450 L140,280 Z" fill="#8BC34A"/>
|
| 141 |
+
|
| 142 |
+
<path d="M60,230 L60,280 L140,280 L140,230 Z" fill="#FDD835"/>
|
| 143 |
+
<circle cx="100" cy="255" r="25" fill="#FDD835"/>
|
| 144 |
+
|
| 145 |
+
<path d="M75,50 Q100,40 125,50 L125,70 Q100,80 75,70 Z" fill="#555555"/>
|
| 146 |
+
<path d="M80,70 L60,230 L140,230 L120,70 Z" fill="#555555"/>
|
| 147 |
+
|
| 148 |
+
<path d="M80,70 Q100,60 120,70 L120,70 Q100,80 80,70 Z" fill="#555555"/>
|
| 149 |
+
</svg>`;
|
| 150 |
+
|
| 151 |
+
// 辅助函数:将字符串转换为 base64
|
| 152 |
+
function svgToBase64(svg) {
|
| 153 |
+
// 移除所有换行符和多余的空格
|
| 154 |
+
const cleanSvg = svg.replace(/\s+/g, ' ').trim();
|
| 155 |
+
// 使用 encodeURIComponent 处理 Unicode 字符
|
| 156 |
+
const encoded = encodeURIComponent(cleanSvg);
|
| 157 |
+
return 'data:image/svg+xml;base64,' + btoa(unescape(encoded));
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// 将 SVG 转换为 base64
|
| 161 |
+
const bottleImageBase64 = svgToBase64(bottleImageSVG);
|
| 162 |
+
|
| 163 |
+
// 从SVG的viewBox中获取实际比例
|
| 164 |
+
const svgViewBox = bottleImageSVG.match(/viewBox="([^"]+)"/)[1].split(' ').map(Number);
|
| 165 |
+
const svgWidth = svgViewBox[2];
|
| 166 |
+
const svgHeight = svgViewBox[3];
|
| 167 |
+
const svgAspectRatio = svgHeight / svgWidth;
|
| 168 |
+
|
| 169 |
+
// 计算基础间距(在 makeChart 函数开始处添加)
|
| 170 |
+
const baseBottleWidth = xScale.bandwidth();
|
| 171 |
+
const baseBottleHeight = baseBottleWidth * svgAspectRatio;
|
| 172 |
+
const spacingAfterBottle = baseBottleWidth * 0.1; // 瓶子和图标之间的间距
|
| 173 |
+
const spacingBetweenIconLabel = baseBottleWidth * 0.15; // 图标和标签之间的间距
|
| 174 |
+
|
| 175 |
+
// 开始添加瓶子图像和蒙版
|
| 176 |
+
chart.selectAll(".bottle-group")
|
| 177 |
+
.data(sortedData)
|
| 178 |
+
.enter()
|
| 179 |
+
.append("g")
|
| 180 |
+
.attr("class", "bottle-group")
|
| 181 |
+
.attr("transform", d => `translate(${xScale(d[xField])}, ${bottleTopY})`)
|
| 182 |
+
.each(function(d, i) {
|
| 183 |
+
const group = d3.select(this);
|
| 184 |
+
const bottleWidth = xScale.bandwidth();
|
| 185 |
+
// 根据SVG的实际比例计算瓶子高度
|
| 186 |
+
const bottleHeight = bottleWidth * svgAspectRatio;
|
| 187 |
+
// 判断是否为最大值
|
| 188 |
+
const isMaxValue = +d[yField] === yMax;
|
| 189 |
+
const bottlePercentage = isMaxValue ? 1 : Math.max(0, (+d[yField] / yMax));
|
| 190 |
+
|
| 191 |
+
// 定义裁剪路径
|
| 192 |
+
const clipId = `clip-${i}`;
|
| 193 |
+
group.append("clipPath")
|
| 194 |
+
.attr("id", clipId)
|
| 195 |
+
.append("rect")
|
| 196 |
+
.attr("x", 0)
|
| 197 |
+
.attr("y", bottleHeight * (1 - bottlePercentage))
|
| 198 |
+
.attr("width", bottleWidth)
|
| 199 |
+
.attr("height", bottleHeight * bottlePercentage);
|
| 200 |
+
|
| 201 |
+
// 添加半透明的背景瓶子
|
| 202 |
+
group.append("image")
|
| 203 |
+
.attr("xlink:href", bottleImageBase64)
|
| 204 |
+
.attr("width", bottleWidth)
|
| 205 |
+
.attr("height", bottleHeight)
|
| 206 |
+
.attr("preserveAspectRatio", "xMidYMid meet")
|
| 207 |
+
.style("opacity", 0.15);
|
| 208 |
+
|
| 209 |
+
// 添加裁剪后的前景瓶子
|
| 210 |
+
group.append("image")
|
| 211 |
+
.attr("xlink:href", bottleImageBase64)
|
| 212 |
+
.attr("width", bottleWidth)
|
| 213 |
+
.attr("height", bottleHeight)
|
| 214 |
+
.attr("preserveAspectRatio", "xMidYMid meet")
|
| 215 |
+
.attr("clip-path", `url(#${clipId})`);
|
| 216 |
+
|
| 217 |
+
// 计算标签方块的大小和位置
|
| 218 |
+
const squareSize = bottleWidth * 0.6;
|
| 219 |
+
const squareX = (bottleWidth - squareSize) / 2;
|
| 220 |
+
const squareY = bottleHeight * (2/3) - (squareSize / 2);
|
| 221 |
+
|
| 222 |
+
// 添加正方形背景
|
| 223 |
+
group.append("rect")
|
| 224 |
+
.attr("class", "value-bg")
|
| 225 |
+
.attr("x", squareX)
|
| 226 |
+
.attr("y", squareY)
|
| 227 |
+
.attr("width", squareSize)
|
| 228 |
+
.attr("height", squareSize)
|
| 229 |
+
.attr("fill", "#f5f5f5")
|
| 230 |
+
.attr("opacity", 0.8)
|
| 231 |
+
.attr("rx", 5)
|
| 232 |
+
.attr("ry", 5);
|
| 233 |
+
|
| 234 |
+
// 格式化值
|
| 235 |
+
|
| 236 |
+
const formattedValue = `${chartUtils.format.autoText(+d[yField])}${valueUnit}`;
|
| 237 |
+
// 我们将在下面计算实际字体大小,先使用基础大小创建文本元素
|
| 238 |
+
const baseValueFontSize = 16;
|
| 239 |
+
|
| 240 |
+
// 添加数值文本但先不设置字体大小
|
| 241 |
+
const valueText = group.append("text")
|
| 242 |
+
.attr("class", "value-label")
|
| 243 |
+
.attr("x", bottleWidth / 2)
|
| 244 |
+
.attr("y", squareY + (squareSize * 0.6)) // 先设置一个位置,稍后会调整
|
| 245 |
+
.attr("text-anchor", "middle")
|
| 246 |
+
.style("font-family", typography.label.font_family)
|
| 247 |
+
.style("font-weight", "bold")
|
| 248 |
+
.style("fill", "#333")
|
| 249 |
+
.style("font-size", `${baseValueFontSize}px`) // 先设置基础字体大小
|
| 250 |
+
.text(formattedValue);
|
| 251 |
+
|
| 252 |
+
// 将值标签添加到数组中,以便后续统一调整
|
| 253 |
+
valueLabels.push({
|
| 254 |
+
element: valueText,
|
| 255 |
+
value: formattedValue,
|
| 256 |
+
squareSize: squareSize
|
| 257 |
+
});
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
// ---------- 8. 添加国旗和国家名称 (修改后) ----------
|
| 261 |
+
const baseLabelFontSize = 14; // 基础字体大小
|
| 262 |
+
// 文本换行辅助函数 (添加 alignment 参数)
|
| 263 |
+
function wrapText(text, str, width, lineHeight = 1.1, alignment = 'middle') {
|
| 264 |
+
const words = str.split(/\s+/).reverse(); // 按空格分割单词
|
| 265 |
+
let word;
|
| 266 |
+
let line = [];
|
| 267 |
+
let lineNumber = 0;
|
| 268 |
+
const initialY = parseFloat(text.attr("y")); // 获取原始y坐标
|
| 269 |
+
const initialX = parseFloat(text.attr("x")); // 获取原始x坐标
|
| 270 |
+
const actualFontSize = parseFloat(text.style("font-size")); // 获取实际应用的字体大小
|
| 271 |
+
|
| 272 |
+
text.text(null); // 清空现有文本
|
| 273 |
+
|
| 274 |
+
let tspans = []; // 存储最终要渲染的行
|
| 275 |
+
|
| 276 |
+
// 优先按单词换行
|
| 277 |
+
if (words.length > 1) {
|
| 278 |
+
let currentLine = [];
|
| 279 |
+
while (word = words.pop()) {
|
| 280 |
+
currentLine.push(word);
|
| 281 |
+
const isOverflow = chartUtils.text.measure(null, currentLine.join(" "), {
|
| 282 |
+
fontFamily: text.style("font-family"),
|
| 283 |
+
fontSize: parseFloat(text.style("font-size")),
|
| 284 |
+
fontWeight: text.style("font-weight")
|
| 285 |
+
}).width > width;
|
| 286 |
+
|
| 287 |
+
if (isOverflow && currentLine.length > 1) {
|
| 288 |
+
currentLine.pop(); // 回退一个词
|
| 289 |
+
tspans.push(currentLine.join(" ")); // 添加完成的行
|
| 290 |
+
currentLine = [word]; // 新行以当前词开始
|
| 291 |
+
lineNumber++;
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
// 添加最后一行
|
| 295 |
+
if (currentLine.length > 0) {
|
| 296 |
+
tspans.push(currentLine.join(" "));
|
| 297 |
+
}
|
| 298 |
+
} else { // 如果没有空格或只有一个词,则按字符换行
|
| 299 |
+
const chars = str.split('');
|
| 300 |
+
let currentLine = '';
|
| 301 |
+
for (let i = 0; i < chars.length; i++) {
|
| 302 |
+
const nextLine = currentLine + chars[i];
|
| 303 |
+
const isOverflow = chartUtils.text.measure(null, nextLine, {
|
| 304 |
+
fontFamily: text.style("font-family"),
|
| 305 |
+
fontSize: parseFloat(text.style("font-size")),
|
| 306 |
+
fontWeight: text.style("font-weight")
|
| 307 |
+
}).width > width;
|
| 308 |
+
|
| 309 |
+
if (isOverflow && currentLine.length > 0) { // 如果加了新字符就超长了,并且当前行不为空
|
| 310 |
+
tspans.push(currentLine); // 添加当前行
|
| 311 |
+
currentLine = chars[i]; // 新行从这个字符开始
|
| 312 |
+
lineNumber++;
|
| 313 |
+
} else {
|
| 314 |
+
currentLine = nextLine; // 没超长就继续加字符
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
// 添加最后一行
|
| 318 |
+
if (currentLine.length > 0) {
|
| 319 |
+
tspans.push(currentLine);
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// 计算总行数
|
| 324 |
+
const totalLines = tspans.length;
|
| 325 |
+
let startDy = 0;
|
| 326 |
+
|
| 327 |
+
// 根据对齐方式计算起始偏移
|
| 328 |
+
if (alignment === 'middle') {
|
| 329 |
+
// 垂直居中:向上移动半行*(总行数-1)
|
| 330 |
+
startDy = -( (totalLines - 1) * lineHeight / 2);
|
| 331 |
+
} else if (alignment === 'bottom') {
|
| 332 |
+
// 底部对齐:计算总高度,向上移动 总高度 - 单行高度(近似)
|
| 333 |
+
// 注意:em单位是相对于字体大小的,这里用 lineHeight * actualFontSize 近似计算像素高度
|
| 334 |
+
const totalHeightEm = totalLines * lineHeight;
|
| 335 |
+
startDy = -(totalHeightEm - lineHeight); // 将底部对齐到原始y
|
| 336 |
+
}
|
| 337 |
+
// 如果是 'top' 对齐,startDy 保持为 0,即第一行基线在原始y位置
|
| 338 |
+
// 其他对齐方式(如 'top')可以保持 startDy 为 0
|
| 339 |
+
|
| 340 |
+
// 创建所有行的tspan元素
|
| 341 |
+
tspans.forEach((lineText, i) => {
|
| 342 |
+
text.append("tspan")
|
| 343 |
+
.attr("x", initialX) // x坐标与父<text>相同
|
| 344 |
+
.attr("dy", (i === 0 ? startDy : lineHeight) + "em") // 第一行应用起始偏移,后续行应用行高
|
| 345 |
+
.text(lineText);
|
| 346 |
+
});
|
| 347 |
+
}
|
| 348 |
+
// 计算所有维度标签在基础字体大小下的宽度
|
| 349 |
+
const dimensionWidths = sortedData.map(d => chartUtils.text.measure(svg, d[xField], {
|
| 350 |
+
fontFamily: typography.label.font_family,
|
| 351 |
+
fontSize: baseLabelFontSize
|
| 352 |
+
}).width);
|
| 353 |
+
|
| 354 |
+
const maxAvailableWidth = xScale.bandwidth() * 1.2;
|
| 355 |
+
|
| 356 |
+
// 计算是否需要缩小字体以及缩放比例
|
| 357 |
+
const maxDimensionWidth = Math.max(...dimensionWidths);
|
| 358 |
+
const labelScaleFactor = maxDimensionWidth > maxAvailableWidth ? maxAvailableWidth / maxDimensionWidth : 1;
|
| 359 |
+
|
| 360 |
+
// 确定最终的统一字体大小(增大1.5倍)
|
| 361 |
+
const finalLabelFontSize = baseLabelFontSize * labelScaleFactor * 1.5;
|
| 362 |
+
|
| 363 |
+
// 开始添加国旗和标签组
|
| 364 |
+
chart.selectAll(".flag-label-group")
|
| 365 |
+
.data(sortedData)
|
| 366 |
+
.enter()
|
| 367 |
+
.append("g")
|
| 368 |
+
.attr("class", "flag-label-group")
|
| 369 |
+
// 根据实际瓶子高度调整位置
|
| 370 |
+
.attr("transform", d => `translate(${xScale(d[xField]) + baseBottleWidth / 2}, ${bottleTopY + baseBottleHeight + spacingAfterBottle})`)
|
| 371 |
+
.each(function(d, i) {
|
| 372 |
+
const group = d3.select(this);
|
| 373 |
+
|
| 374 |
+
// 计算图标尺寸
|
| 375 |
+
const iconWidth = baseBottleWidth;
|
| 376 |
+
const iconHeight = iconWidth * 0.75;
|
| 377 |
+
|
| 378 |
+
// 添加国旗(如果数据中提供了图片字段和对应的值)
|
| 379 |
+
if (images.field && images.field[d[xField]]) {
|
| 380 |
+
group.append("image")
|
| 381 |
+
.attr("xlink:href", images.field[d[xField]])
|
| 382 |
+
.attr("x", -iconWidth / 2)
|
| 383 |
+
.attr("y", 0)
|
| 384 |
+
.attr("width", iconWidth)
|
| 385 |
+
.attr("height", iconHeight)
|
| 386 |
+
.attr("preserveAspectRatio", "xMidYMid meet");
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// 添加国家名称文本
|
| 390 |
+
const label = group.append("text")
|
| 391 |
+
.attr("class", "country-label")
|
| 392 |
+
.attr("x", 0)
|
| 393 |
+
.attr("y", iconHeight + spacingBetweenIconLabel)
|
| 394 |
+
.attr("text-anchor", "middle")
|
| 395 |
+
.style("font-family", typography.label.font_family)
|
| 396 |
+
.style("font-size", `${finalLabelFontSize}px`)
|
| 397 |
+
.style("fill", colorResolver.text({ fallback: "#333333" }).value)
|
| 398 |
+
.text(d[xField]);
|
| 399 |
+
|
| 400 |
+
// 如果需要换行,重新应用换行
|
| 401 |
+
const labelMaxWidth = baseBottleWidth * 1.05;
|
| 402 |
+
if (chartUtils.text.measure(chart, d[xField], {
|
| 403 |
+
fontFamily: typography.label.font_family,
|
| 404 |
+
fontSize: finalLabelFontSize
|
| 405 |
+
}).width > labelMaxWidth) {
|
| 406 |
+
wrapText(label, d[xField].toString(), labelMaxWidth, 1.1, 'top');
|
| 407 |
+
}
|
| 408 |
+
});
|
| 409 |
+
|
| 410 |
+
// ---------- 9. 统一调整所有值标签的字体大小 ----------
|
| 411 |
+
// 创建临时文本元素来测量值标签文本宽度
|
| 412 |
+
|
| 413 |
+
const baseValueFontSize = 16;
|
| 414 |
+
// 计算所有值标签在基础字体大小下的实际宽度
|
| 415 |
+
const valueTextWidths = valueLabels.map(item => {
|
| 416 |
+
return chartUtils.text.measure(svg, item.value, {
|
| 417 |
+
fontFamily: typography.label.font_family,
|
| 418 |
+
fontSize: `${baseValueFontSize}px`,
|
| 419 |
+
fontWeight: "bold"
|
| 420 |
+
}).width;
|
| 421 |
+
});
|
| 422 |
+
|
| 423 |
+
// 计算每个标签所需的缩放比例(方形宽度 / 文本宽度)
|
| 424 |
+
const valueScaleFactors = valueLabels.map((item, i) => {
|
| 425 |
+
// 使用60%的方形宽度作为最大可用宽度
|
| 426 |
+
const maxWidth = item.squareSize * 1; // 留一些边距
|
| 427 |
+
return maxWidth / valueTextWidths[i];
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
// 取最小的缩放比例,确保所有值标签使用相同的字体大小
|
| 431 |
+
const minValueScaleFactor = Math.min(...valueScaleFactors, 1); // 不要放大,只缩小
|
| 432 |
+
|
| 433 |
+
// 计算最终的统一字体大小
|
| 434 |
+
const finalValueFontSize = Math.min(baseValueFontSize * minValueScaleFactor, baseValueFontSize);
|
| 435 |
+
|
| 436 |
+
// 应用统一的字体大小到所有值标签
|
| 437 |
+
valueLabels.forEach(item => {
|
| 438 |
+
item.element
|
| 439 |
+
.style("font-size", `${finalValueFontSize}px`);
|
| 440 |
+
});
|
| 441 |
+
|
| 442 |
+
// 返回SVG节点
|
| 443 |
+
return svg.node();
|
| 444 |
+
}
|