Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files
modules/chart_engine/template/d3-js/type2_vertical_stacked_bar_chart/vertical_stacked_bar_plain_chart_01.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
REQUIREMENTS_BEGIN
|
| 3 |
+
{
|
| 4 |
+
"chart_type": "Vertical Stacked Bar Chart",
|
| 5 |
+
"chart_name": "vertical_stacked_bar_plain_chart_01",
|
| 6 |
+
"required_fields": ["x", "y", "group"],
|
| 7 |
+
"required_fields_type": [["categorical"], ["numerical"], ["categorical"]],
|
| 8 |
+
"required_fields_range": [[3, 12], [0, 100], [3, 20]],
|
| 9 |
+
"required_fields_icons": [],
|
| 10 |
+
"required_other_icons": [],
|
| 11 |
+
"required_fields_colors": ["group"],
|
| 12 |
+
"required_other_colors": ["primary", "secondary", "background"],
|
| 13 |
+
"supported_effects": ["gradient", "opacity"],
|
| 14 |
+
"min_height": 600,
|
| 15 |
+
"min_width": 800,
|
| 16 |
+
"background": "light",
|
| 17 |
+
"icon_mark": "none",
|
| 18 |
+
"icon_label": "none",
|
| 19 |
+
"has_x_axis": "yes",
|
| 20 |
+
"has_y_axis": "yes"
|
| 21 |
+
}
|
| 22 |
+
REQUIREMENTS_END
|
| 23 |
+
*/
|
| 24 |
+
|
| 25 |
+
function makeChart(containerSelector, data) {
|
| 26 |
+
const colorResolver = chartUtils.color.resolver(data);
|
| 27 |
+
// ---------- 1. 数据准备阶段 ----------
|
| 28 |
+
|
| 29 |
+
// 提取数据和配置
|
| 30 |
+
const jsonData = data; // 完整的JSON数据对象
|
| 31 |
+
const chartData = jsonData.data.data; // 实际数据点数组
|
| 32 |
+
const variables = jsonData.variables || {}; // 图表配置
|
| 33 |
+
const typography = jsonData.typography || { // 字体设置,如果不存在则使用默认值
|
| 34 |
+
title: { font_family: "Arial", font_size: "18px", font_weight: "bold" },
|
| 35 |
+
label: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
|
| 36 |
+
description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
|
| 37 |
+
annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
|
| 38 |
+
};
|
| 39 |
+
const colors = jsonData.colors || {
|
| 40 |
+
text_color: "#333333",
|
| 41 |
+
other: {
|
| 42 |
+
primary: "#D32F2F", // Red for "Still active"
|
| 43 |
+
secondary: "#AAAAAA", // Gray for "Ended"
|
| 44 |
+
background: "#F0F0F0"
|
| 45 |
+
}
|
| 46 |
+
}; // 颜色设置
|
| 47 |
+
const dataColumns = chartUtils.schema.columns(jsonData); // 数据列定义
|
| 48 |
+
|
| 49 |
+
// 设置视觉效果变量的默认值
|
| 50 |
+
variables.has_shadow = variables.has_shadow || false;
|
| 51 |
+
variables.has_stroke = variables.has_stroke || false;
|
| 52 |
+
|
| 53 |
+
// 清空容器
|
| 54 |
+
d3.select(containerSelector).html("");
|
| 55 |
+
|
| 56 |
+
// 添加数值格式化函数
|
| 57 |
+
|
| 58 |
+
// ---------- 2. 尺寸和布局设置 ----------
|
| 59 |
+
|
| 60 |
+
// 设置图表总尺寸
|
| 61 |
+
const width = variables.width || 600;
|
| 62 |
+
const height = variables.height || 400;
|
| 63 |
+
|
| 64 |
+
// 设置边距
|
| 65 |
+
const margin = {
|
| 66 |
+
top: 50,
|
| 67 |
+
right: 30,
|
| 68 |
+
bottom: 80,
|
| 69 |
+
left: 40
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
// 计算实际绘图区域大小
|
| 73 |
+
const chartWidth = width - margin.left - margin.right;
|
| 74 |
+
const chartHeight = height - margin.top - margin.bottom;
|
| 75 |
+
|
| 76 |
+
// ---------- 3. 提取字段名和单位 ----------
|
| 77 |
+
|
| 78 |
+
// 根据数据列获取字段名
|
| 79 |
+
const xField = chartUtils.schema.channel(jsonData, "x").key || "period";
|
| 80 |
+
const yField = chartUtils.schema.channel(jsonData, "y").key || "value";
|
| 81 |
+
const groupField = chartUtils.schema.channel(jsonData, "group").key || "status";
|
| 82 |
+
|
| 83 |
+
// 获取字段单位(如果存在)
|
| 84 |
+
let xUnit = "";
|
| 85 |
+
let yUnit = "";
|
| 86 |
+
let groupUnit = "";
|
| 87 |
+
|
| 88 |
+
if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
|
| 89 |
+
xUnit = chartUtils.schema.channel(jsonData, "x").unit;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
|
| 93 |
+
yUnit = chartUtils.schema.channel(jsonData, "y").unit;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
if (chartUtils.schema.channel(jsonData, "group").unit !== "none") {
|
| 97 |
+
groupUnit = chartUtils.schema.channel(jsonData, "group").unit;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// ---------- 4. 数据处理 ----------
|
| 101 |
+
|
| 102 |
+
// 获取所有唯一的组值
|
| 103 |
+
const groups = Array.from(new Set(chartData.map(d => d[groupField])));
|
| 104 |
+
|
| 105 |
+
// 处理数据为堆叠格式
|
| 106 |
+
const groupedData = d3.group(chartData, d => d[xField]);
|
| 107 |
+
const processedData = Array.from(groupedData, ([key, values]) => {
|
| 108 |
+
const obj = { period: key };
|
| 109 |
+
groups.forEach(group => {
|
| 110 |
+
obj[group] = d3.sum(values.filter(d => d[groupField] === group), d => +d[yField]);
|
| 111 |
+
});
|
| 112 |
+
obj.total = d3.sum(values, d => +d[yField]);
|
| 113 |
+
return obj;
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// 创建堆叠生成器
|
| 117 |
+
const stack = d3.stack()
|
| 118 |
+
.keys(groups)
|
| 119 |
+
.order(d3.stackOrderNone)
|
| 120 |
+
.offset(d3.stackOffsetNone);
|
| 121 |
+
|
| 122 |
+
// 生成堆叠数据
|
| 123 |
+
const stackedData = stack(processedData);
|
| 124 |
+
|
| 125 |
+
// ---------- 5. 创建比例尺 ----------
|
| 126 |
+
|
| 127 |
+
// X轴比例尺 - 使用时间段作为分类
|
| 128 |
+
const xScale = d3.scaleBand()
|
| 129 |
+
.domain(processedData.map(d => d.period))
|
| 130 |
+
.range([0, chartWidth])
|
| 131 |
+
.padding(0.3);
|
| 132 |
+
|
| 133 |
+
// Y轴比例尺 - 使用数值
|
| 134 |
+
const yScale = d3.scaleLinear()
|
| 135 |
+
.domain([0, d3.max(processedData, d => d.total)])
|
| 136 |
+
.range([chartHeight, 0])
|
| 137 |
+
.nice();
|
| 138 |
+
|
| 139 |
+
const colorScale = colorResolver.scale(groups, { palette: "category10" })
|
| 140 |
+
|
| 141 |
+
// 确定标签的最大长度:
|
| 142 |
+
|
| 143 |
+
let minXLabelRatio = 1.0
|
| 144 |
+
const maxXLabelWidth = xScale.bandwidth() * 1.03
|
| 145 |
+
|
| 146 |
+
chartData.forEach(d => {
|
| 147 |
+
// x label
|
| 148 |
+
const xLabelText = String(d[xField])
|
| 149 |
+
let currentWidth = chartUtils.text.measure(null, xLabelText).width
|
| 150 |
+
if (currentWidth > maxXLabelWidth) {
|
| 151 |
+
minXLabelRatio = Math.min(minXLabelRatio, maxXLabelWidth / currentWidth)
|
| 152 |
+
}
|
| 153 |
+
})
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
// ---------- 6. 创建SVG容器 ----------
|
| 157 |
+
|
| 158 |
+
const svg = d3.select(containerSelector)
|
| 159 |
+
.append("svg")
|
| 160 |
+
.attr("width", "100%")
|
| 161 |
+
.attr("height", height)
|
| 162 |
+
.attr("viewBox", `0 0 ${width} ${height}`)
|
| 163 |
+
.attr("style", "max-width: 100%; height: auto;")
|
| 164 |
+
.attr("xmlns", "http://www.w3.org/2000/svg")
|
| 165 |
+
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
| 166 |
+
|
| 167 |
+
// 添加图表主体容器
|
| 168 |
+
const chartGroup = svg.append("g")
|
| 169 |
+
.attr("transform", `translate(${margin.left}, ${margin.top})`);
|
| 170 |
+
|
| 171 |
+
// ---------- 7. 绘制图表元素 ----------
|
| 172 |
+
|
| 173 |
+
// 绘制堆叠的条形
|
| 174 |
+
const layers = chartGroup.selectAll(".layer")
|
| 175 |
+
.data(stackedData)
|
| 176 |
+
.enter().append("g")
|
| 177 |
+
.attr("class", "layer")
|
| 178 |
+
.style("fill", (d) => colorScale(d.key));
|
| 179 |
+
|
| 180 |
+
layers.selectAll("rect")
|
| 181 |
+
.data(d => d)
|
| 182 |
+
.enter().append("rect")
|
| 183 |
+
.attr("x", d => xScale(d.data.period))
|
| 184 |
+
.attr("y", d => yScale(d[1]))
|
| 185 |
+
.attr("height", d => yScale(d[0]) - yScale(d[1]))
|
| 186 |
+
.attr("width", xScale.bandwidth())
|
| 187 |
+
.style("stroke", variables.has_stroke ? "#ffffff" : "none")
|
| 188 |
+
.style("stroke-width", variables.has_stroke ? 1 : 0);
|
| 189 |
+
|
| 190 |
+
// 添加数值标注
|
| 191 |
+
layers.selectAll("text")
|
| 192 |
+
.data(d => d)
|
| 193 |
+
.enter().append("text")
|
| 194 |
+
.attr("x", d => xScale(d.data.period) + xScale.bandwidth() / 2)
|
| 195 |
+
.attr("y", d => {
|
| 196 |
+
const height = yScale(d[0]) - yScale(d[1]);
|
| 197 |
+
return yScale(d[1]) + height / 2;
|
| 198 |
+
})
|
| 199 |
+
.attr("text-anchor", "middle")
|
| 200 |
+
.attr("dominant-baseline", "middle")
|
| 201 |
+
.style("fill", "#ffffff")
|
| 202 |
+
.style("font-family", typography.annotation.font_family)
|
| 203 |
+
.style("font-size", typography.annotation.font_size)
|
| 204 |
+
.text(d => {
|
| 205 |
+
const value = d[1] - d[0];
|
| 206 |
+
const height = yScale(d[0]) - yScale(d[1]);
|
| 207 |
+
// 只在高度大于16且值大于0时显示文本
|
| 208 |
+
return (height > 16 && value > 0) ? chartUtils.format.autoText(value) + (yUnit ? ` ${yUnit}` : '') : '';
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
// 添加X轴
|
| 212 |
+
const xAxis = chartGroup.append("g")
|
| 213 |
+
.attr("class", "x-axis")
|
| 214 |
+
.attr("transform", `translate(0, ${chartHeight})`)
|
| 215 |
+
.call(d3.axisBottom(xScale)
|
| 216 |
+
.tickSize(0) // 移除刻度线
|
| 217 |
+
.tickPadding(10)) // 增加文字和轴的间距
|
| 218 |
+
.call(g => g.select(".domain").remove()) // 移除轴线
|
| 219 |
+
.selectAll("text")
|
| 220 |
+
.style("text-anchor", minXLabelRatio < 1.0 ? "end" : "middle")
|
| 221 |
+
.attr("transform", minXLabelRatio < 1.0 ? "rotate(-45)" : "rotate(0)") // 根据minXLabelRatio调整文字旋转
|
| 222 |
+
.style("font-family", typography.label.font_family)
|
| 223 |
+
.style("font-size", typography.label.font_size)
|
| 224 |
+
.style("fill", colorResolver.text({ fallback: "#333333" }).value);
|
| 225 |
+
// 添加图例 - 放在图表上方
|
| 226 |
+
const legendGroup = svg.append("g")
|
| 227 |
+
.attr("transform", `translate(0, -50)`);
|
| 228 |
+
|
| 229 |
+
// 计算字段名宽度并添加间距
|
| 230 |
+
const titleWidth = groupField.length * 10;
|
| 231 |
+
const titleMargin = 15;
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
const legendSize = chartUtils.legend.draw(legendGroup, groups, colors, {
|
| 235 |
+
x: titleWidth + titleMargin,
|
| 236 |
+
y: 0,
|
| 237 |
+
fontSize: 14,
|
| 238 |
+
fontWeight: "bold",
|
| 239 |
+
align: "left",
|
| 240 |
+
maxWidth: chartWidth - titleWidth - titleMargin,
|
| 241 |
+
shape: "rect",
|
| 242 |
+
});
|
| 243 |
+
|
| 244 |
+
// 添加字段名称
|
| 245 |
+
legendGroup.append("text")
|
| 246 |
+
.attr("x", 0)
|
| 247 |
+
.attr("y", legendSize.height / 2)
|
| 248 |
+
.attr("dominant-baseline", "middle")
|
| 249 |
+
.attr("fill", "#333")
|
| 250 |
+
.style("font-size", "16px")
|
| 251 |
+
.style("font-weight", "bold")
|
| 252 |
+
.text(groupField);
|
| 253 |
+
|
| 254 |
+
// 将图例组向上移动 height/2, 并居中
|
| 255 |
+
legendGroup.attr("transform", `translate(${(chartWidth - legendSize.width - titleWidth - titleMargin) / 2}, 0)`);
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
return svg.node();
|
| 259 |
+
}
|
modules/chart_engine/template/d3-js/type2_vertical_stacked_bar_chart/vertical_stacked_bar_plain_chart_02.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
REQUIREMENTS_BEGIN
|
| 3 |
+
{
|
| 4 |
+
"chart_type": "Vertical Stacked Bar Chart",
|
| 5 |
+
"chart_name": "vertical_stacked_bar_plain_chart_02",
|
| 6 |
+
"required_fields": ["x", "y", "group"],
|
| 7 |
+
"required_fields_type": [["categorical"], ["numerical"], ["categorical"]],
|
| 8 |
+
"required_fields_range": [[3, 12], [0, 100], [3, 20]],
|
| 9 |
+
"required_fields_icons": [],
|
| 10 |
+
"required_other_icons": [],
|
| 11 |
+
"required_fields_colors": ["group"],
|
| 12 |
+
"required_other_colors": ["primary", "secondary", "background"],
|
| 13 |
+
"supported_effects": ["gradient", "opacity"],
|
| 14 |
+
"min_height": 600,
|
| 15 |
+
"min_width": 800,
|
| 16 |
+
"background": "light",
|
| 17 |
+
"icon_mark": "none",
|
| 18 |
+
"icon_label": "none",
|
| 19 |
+
"has_x_axis": "yes",
|
| 20 |
+
"has_y_axis": "yes"
|
| 21 |
+
}
|
| 22 |
+
REQUIREMENTS_END
|
| 23 |
+
*/
|
| 24 |
+
|
| 25 |
+
// vertical stacked bar chart plain chart#2
|
| 26 |
+
function makeChart(containerSelector, data) {
|
| 27 |
+
const colorResolver = chartUtils.color.resolver(data);
|
| 28 |
+
// ---------- 1. 数据准备阶段 ----------
|
| 29 |
+
|
| 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: "18px", font_weight: "bold" },
|
| 36 |
+
label: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
|
| 37 |
+
description: { font_family: "Arial", font_size: "14px", font_weight: "normal" },
|
| 38 |
+
annotation: { font_family: "Arial", font_size: "12px", font_weight: "normal" }
|
| 39 |
+
};
|
| 40 |
+
const colors = jsonData.colors || {
|
| 41 |
+
text_color: "#333333",
|
| 42 |
+
other: {
|
| 43 |
+
primary: "#D32F2F", // Red for "Still active"
|
| 44 |
+
secondary: "#AAAAAA", // Gray for "Ended"
|
| 45 |
+
background: "#F0F0F0"
|
| 46 |
+
}
|
| 47 |
+
}; // 颜色设置
|
| 48 |
+
const dataColumns = chartUtils.schema.columns(jsonData); // 数据列定义
|
| 49 |
+
|
| 50 |
+
// 设置视觉效果变量的默认值
|
| 51 |
+
variables.has_shadow = variables.has_shadow || false;
|
| 52 |
+
variables.has_stroke = variables.has_stroke || false;
|
| 53 |
+
|
| 54 |
+
// 清空容器
|
| 55 |
+
d3.select(containerSelector).html("");
|
| 56 |
+
|
| 57 |
+
// 添加数值格式化函数
|
| 58 |
+
|
| 59 |
+
// ---------- 2. 尺寸和布局设置 ----------
|
| 60 |
+
|
| 61 |
+
// 设置图表总尺寸
|
| 62 |
+
const width = variables.width || 600;
|
| 63 |
+
const height = variables.height || 400;
|
| 64 |
+
|
| 65 |
+
// 设置边距
|
| 66 |
+
const margin = {
|
| 67 |
+
top: 50,
|
| 68 |
+
right: 30,
|
| 69 |
+
bottom: 80,
|
| 70 |
+
left: 40
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
// 计算实际绘图区域大小
|
| 74 |
+
const chartWidth = width - margin.left - margin.right;
|
| 75 |
+
const chartHeight = height - margin.top - margin.bottom;
|
| 76 |
+
|
| 77 |
+
// ---------- 3. 提取字段名和单位 ----------
|
| 78 |
+
|
| 79 |
+
// 根据数据列获取字段名
|
| 80 |
+
const xField = chartUtils.schema.channel(jsonData, "x").key || "period";
|
| 81 |
+
const yField = chartUtils.schema.channel(jsonData, "y").key || "value";
|
| 82 |
+
const groupField = chartUtils.schema.channel(jsonData, "group").key || "status";
|
| 83 |
+
|
| 84 |
+
// 获取字段单位(如果存在)
|
| 85 |
+
let xUnit = "";
|
| 86 |
+
let yUnit = "";
|
| 87 |
+
let groupUnit = "";
|
| 88 |
+
|
| 89 |
+
if (chartUtils.schema.channel(jsonData, "x").unit !== "none") {
|
| 90 |
+
xUnit = chartUtils.schema.channel(jsonData, "x").unit;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if (chartUtils.schema.channel(jsonData, "y").unit !== "none") {
|
| 94 |
+
yUnit = chartUtils.schema.channel(jsonData, "y").unit;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
if (chartUtils.schema.channel(jsonData, "group").unit !== "none") {
|
| 98 |
+
groupUnit = chartUtils.schema.channel(jsonData, "group").unit;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// ---------- 4. 数据处理 ----------
|
| 102 |
+
|
| 103 |
+
// 获取所有唯一的组值
|
| 104 |
+
const groups = Array.from(new Set(chartData.map(d => d[groupField])));
|
| 105 |
+
|
| 106 |
+
// 处理数据为堆叠格式
|
| 107 |
+
const groupedData = d3.group(chartData, d => d[xField]);
|
| 108 |
+
const processedData = Array.from(groupedData, ([key, values]) => {
|
| 109 |
+
const obj = { period: key };
|
| 110 |
+
groups.forEach(group => {
|
| 111 |
+
obj[group] = d3.sum(values.filter(d => d[groupField] === group), d => +d[yField]);
|
| 112 |
+
});
|
| 113 |
+
obj.total = d3.sum(values, d => +d[yField]);
|
| 114 |
+
return obj;
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
// 创建堆叠生成器
|
| 118 |
+
const stack = d3.stack()
|
| 119 |
+
.keys(groups)
|
| 120 |
+
.order(d3.stackOrderNone)
|
| 121 |
+
.offset(d3.stackOffsetNone);
|
| 122 |
+
|
| 123 |
+
// 生成堆叠数据
|
| 124 |
+
const stackedData = stack(processedData);
|
| 125 |
+
|
| 126 |
+
// ---------- 5. 创建比例尺 ----------
|
| 127 |
+
|
| 128 |
+
// X轴比例尺 - 使用时间段作为分类
|
| 129 |
+
const xScale = d3.scaleBand()
|
| 130 |
+
.domain(processedData.map(d => d.period))
|
| 131 |
+
.range([0, chartWidth])
|
| 132 |
+
.padding(0.3);
|
| 133 |
+
|
| 134 |
+
// Y轴比例尺 - 使用数值
|
| 135 |
+
const yScale = d3.scaleLinear()
|
| 136 |
+
.domain([0, d3.max(processedData, d => d.total)])
|
| 137 |
+
.range([chartHeight, 0])
|
| 138 |
+
.nice();
|
| 139 |
+
|
| 140 |
+
const colorScale = colorResolver.scale(groups, { palette: "category10" })
|
| 141 |
+
|
| 142 |
+
// ���定标签的最大长度:
|
| 143 |
+
|
| 144 |
+
let minXLabelRatio = 1.0
|
| 145 |
+
const maxXLabelWidth = xScale.bandwidth() * 1.03
|
| 146 |
+
|
| 147 |
+
chartData.forEach(d => {
|
| 148 |
+
// x label
|
| 149 |
+
const xLabelText = String(d[xField])
|
| 150 |
+
let currentWidth = chartUtils.text.measure(null, xLabelText).width
|
| 151 |
+
if (currentWidth > maxXLabelWidth) {
|
| 152 |
+
minXLabelRatio = Math.min(minXLabelRatio, maxXLabelWidth / currentWidth)
|
| 153 |
+
}
|
| 154 |
+
})
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
// ---------- 6. 创建SVG容器 ----------
|
| 158 |
+
|
| 159 |
+
const svg = d3.select(containerSelector)
|
| 160 |
+
.append("svg")
|
| 161 |
+
.attr("width", "100%")
|
| 162 |
+
.attr("height", height)
|
| 163 |
+
.attr("viewBox", `0 0 ${width} ${height}`)
|
| 164 |
+
.attr("style", "max-width: 100%; height: auto;")
|
| 165 |
+
.attr("xmlns", "http://www.w3.org/2000/svg")
|
| 166 |
+
.attr("xmlns:xlink", "http://www.w3.org/1999/xlink");
|
| 167 |
+
|
| 168 |
+
// 添加图表主体容器
|
| 169 |
+
const chartGroup = svg.append("g")
|
| 170 |
+
.attr("transform", `translate(${margin.left}, ${margin.top})`);
|
| 171 |
+
|
| 172 |
+
// ---------- 7. 绘制图表元素 ----------
|
| 173 |
+
|
| 174 |
+
// 添加圆角矩形路径生成函数
|
| 175 |
+
function roundedRect(x, y, width, height, radius) {
|
| 176 |
+
// 只对顶部添加圆角
|
| 177 |
+
const topRadius = radius;
|
| 178 |
+
const bottomRadius = 0;
|
| 179 |
+
|
| 180 |
+
// 如果高度小于半径,绘制圆弧
|
| 181 |
+
if (height < radius) {
|
| 182 |
+
// 计算圆弧的角度
|
| 183 |
+
const centerY = y + radius;
|
| 184 |
+
const angle = Math.acos((radius - height) / radius);
|
| 185 |
+
const startX = x + radius - radius * Math.sin(angle);
|
| 186 |
+
const endX = x + width - radius + radius * Math.sin(angle);
|
| 187 |
+
|
| 188 |
+
return `M${x},${y + height}
|
| 189 |
+
L${startX},${y + height}
|
| 190 |
+
A${radius},${radius} 0 0,1 ${endX},${y + height}
|
| 191 |
+
L${x + width},${y + height}
|
| 192 |
+
Z`;
|
| 193 |
+
} else {
|
| 194 |
+
// 标准的圆角矩形(半圆顶部)
|
| 195 |
+
return `M${x},${y + height}
|
| 196 |
+
L${x},${y + topRadius}
|
| 197 |
+
Q${x},${y} ${x + topRadius},${y}
|
| 198 |
+
L${x + width - topRadius},${y}
|
| 199 |
+
Q${x + width},${y} ${x + width},${y + topRadius}
|
| 200 |
+
L${x + width},${y + height}
|
| 201 |
+
Z`;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// 绘制堆叠的条形
|
| 206 |
+
const layers = chartGroup.selectAll(".layer")
|
| 207 |
+
.data(stackedData)
|
| 208 |
+
.enter().append("g")
|
| 209 |
+
.attr("class", "layer")
|
| 210 |
+
.style("fill", (d) => colorScale(d.key));
|
| 211 |
+
|
| 212 |
+
// 计算圆角半径(条形宽度的50%,形成半圆顶部)
|
| 213 |
+
const cornerRadius = xScale.bandwidth() * 0.5;
|
| 214 |
+
|
| 215 |
+
layers.selectAll("path")
|
| 216 |
+
.data(d => d)
|
| 217 |
+
.enter().append("path")
|
| 218 |
+
.attr("d", (d, i, nodes) => {
|
| 219 |
+
const x = xScale(d.data.period);
|
| 220 |
+
const y = yScale(d[1]);
|
| 221 |
+
const width = xScale.bandwidth();
|
| 222 |
+
const height = yScale(d[0]) - yScale(d[1]);
|
| 223 |
+
|
| 224 |
+
// 检查是否是最顶部的条形(在该x位置的最后一个非零高度的条形)
|
| 225 |
+
const layerIndex = d3.select(nodes[i].parentNode).datum().key;
|
| 226 |
+
const isTopLayer = layerIndex === groups[groups.length - 1] ||
|
| 227 |
+
(height > 0 && !stackedData.slice(stackedData.findIndex(layer => layer.key === layerIndex) + 1)
|
| 228 |
+
.some(nextLayer => {
|
| 229 |
+
const nextData = nextLayer.find(item => item.data.period === d.data.period);
|
| 230 |
+
return nextData && (nextData[1] - nextData[0]) > 0;
|
| 231 |
+
}));
|
| 232 |
+
|
| 233 |
+
// 如果是顶层且有一定高度,则添加圆角或圆弧顶部
|
| 234 |
+
if (isTopLayer && height > 0) {
|
| 235 |
+
return roundedRect(x, y, width, height, cornerRadius);
|
| 236 |
+
} else {
|
| 237 |
+
// 普通矩形
|
| 238 |
+
return `M${x},${y} L${x + width},${y} L${x + width},${y + height} L${x},${y + height} Z`;
|
| 239 |
+
}
|
| 240 |
+
})
|
| 241 |
+
.style("stroke", variables.has_stroke ? "#ffffff" : "none")
|
| 242 |
+
.style("stroke-width", variables.has_stroke ? 1 : 0);
|
| 243 |
+
|
| 244 |
+
// 添加数值标注
|
| 245 |
+
layers.selectAll("text")
|
| 246 |
+
.data(d => d)
|
| 247 |
+
.enter().append("text")
|
| 248 |
+
.attr("x", d => xScale(d.data.period) + xScale.bandwidth() / 2)
|
| 249 |
+
.attr("y", d => {
|
| 250 |
+
const height = yScale(d[0]) - yScale(d[1]);
|
| 251 |
+
return yScale(d[1]) + height / 2;
|
| 252 |
+
})
|
| 253 |
+
.attr("text-anchor", "middle")
|
| 254 |
+
.attr("dominant-baseline", "middle")
|
| 255 |
+
.style("fill", "#ffffff")
|
| 256 |
+
.style("font-family", typography.annotation.font_family)
|
| 257 |
+
.style("font-size", typography.annotation.font_size)
|
| 258 |
+
.text(d => {
|
| 259 |
+
const value = d[1] - d[0];
|
| 260 |
+
const height = yScale(d[0]) - yScale(d[1]);
|
| 261 |
+
// 只在高度大于16且值大于0时显示文本
|
| 262 |
+
return (height > 16 && value > 0) ? chartUtils.format.autoText(value) + (yUnit ? ` ${yUnit}` : '') : '';
|
| 263 |
+
});
|
| 264 |
+
|
| 265 |
+
// 添加X轴
|
| 266 |
+
const xAxis = chartGroup.append("g")
|
| 267 |
+
.attr("class", "x-axis")
|
| 268 |
+
.attr("transform", `translate(0, ${chartHeight})`)
|
| 269 |
+
.call(d3.axisBottom(xScale)
|
| 270 |
+
.tickSize(0) // 移除刻度线
|
| 271 |
+
.tickPadding(10)) // 增加文字和轴的间距
|
| 272 |
+
.call(g => g.select(".domain").remove()) // 移除轴线
|
| 273 |
+
.selectAll("text")
|
| 274 |
+
.style("text-anchor", minXLabelRatio < 1.0 ? "end" : "middle")
|
| 275 |
+
.attr("transform", minXLabelRatio < 1.0 ? "rotate(-45)" : "rotate(0)") // 根据minXLabelRatio调整文字旋转
|
| 276 |
+
.style("font-family", typography.label.font_family)
|
| 277 |
+
.style("font-size", typography.label.font_size)
|
| 278 |
+
.style("fill", colorResolver.text({ fallback: "#333333" }).value);
|
| 279 |
+
// 添加图例 - 放在图表上方
|
| 280 |
+
const legendGroup = svg.append("g")
|
| 281 |
+
.attr("transform", `translate(0, -50)`);
|
| 282 |
+
|
| 283 |
+
// 计算字段名宽度并添加间距
|
| 284 |
+
const titleWidth = groupField.length * 10;
|
| 285 |
+
const titleMargin = 15;
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
const legendSize = chartUtils.legend.draw(legendGroup, groups, colors, {
|
| 289 |
+
x: titleWidth + titleMargin,
|
| 290 |
+
y: 0,
|
| 291 |
+
fontSize: 14,
|
| 292 |
+
fontWeight: "bold",
|
| 293 |
+
align: "left",
|
| 294 |
+
maxWidth: chartWidth - titleWidth - titleMargin,
|
| 295 |
+
shape: "rect",
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
// 添加字段名称
|
| 299 |
+
legendGroup.append("text")
|
| 300 |
+
.attr("x", 0)
|
| 301 |
+
.attr("y", legendSize.height / 2)
|
| 302 |
+
.attr("dominant-baseline", "middle")
|
| 303 |
+
.attr("fill", "#333")
|
| 304 |
+
.style("font-size", "16px")
|
| 305 |
+
.style("font-weight", "bold")
|
| 306 |
+
.text(groupField);
|
| 307 |
+
|
| 308 |
+
// 将图例组向上移动 height/2, 并居中
|
| 309 |
+
legendGroup.attr("transform", `translate(${(chartWidth - legendSize.width - titleWidth - titleMargin) / 2}, 0)`);
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
return svg.node();
|
| 313 |
+
}
|