Ray1ee01's picture
Add runnable ChartPipeline Space
216c0a4 verified
Raw
History Blame Contribute Delete
7.95 kB
// Legacy global helpers kept for old D3 templates during migration.
// 解析日期
var parseDate = d => {
if (d instanceof Date) return d;
if (typeof d === 'number') return new Date(d, 0, 1);
if (typeof d === 'string') {
const parts = d.split('-');
// YYYY-MM-DD 格式
if (parts.length === 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
const day = parseInt(parts[2]);
return new Date(year, month, day);
}
// YYYY-MM 格式
if (parts.length === 2) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1;
return new Date(year, month, 1);
}
// YYYY 格式
if (parts.length === 1 && /^\d{4}$/.test(parts[0])) {
const year = parseInt(parts[0]);
return new Date(year, 0, 1);
}
}
return new Date();
};
// 创建智能日期比例尺和刻度
var createXAxisScaleAndTicks = (data, xField, rangeStart = 0, rangeEnd = 100, padding = 0.05) => {
// 解析所有日期
const dates = data.map(d => parseDate(d[xField]));
const xExtent = d3.extent(dates);
const xRange = xExtent[1] - xExtent[0];
const xPadding = xRange * padding;
// 创建比例尺
const xScale = d3.scaleTime()
.domain([
new Date(xExtent[0].getTime() - xPadding),
new Date(xExtent[1].getTime() + xPadding)
])
.range([rangeStart, rangeEnd]);
// 计算日期跨度(毫秒)
const timeSpan = xExtent[1] - xExtent[0];
const daySpan = timeSpan / (1000 * 60 * 60 * 24);
const monthSpan = daySpan / 30;
const yearSpan = daySpan / 365;
// 根据跨度选择合适的时间间隔
let timeInterval;
let formatFunction;
if (yearSpan > 35) {
// 超过35年,每10年一个刻度
timeInterval = d3.timeYear.every(10);
formatFunction = d => d3.timeFormat("%Y")(d);
} else if (yearSpan > 15) {
// 超过15年,每5年一个刻度
timeInterval = d3.timeYear.every(5);
formatFunction = d => d3.timeFormat("%Y")(d);
} else if (yearSpan > 7) {
// 超过7年,每2年一个刻度
timeInterval = d3.timeYear.every(2);
formatFunction = d => d3.timeFormat("%Y")(d);
} else if (yearSpan > 2) {
// 2-7年,每年一个刻度
timeInterval = d3.timeYear.every(1);
formatFunction = d => d3.timeFormat("%Y")(d);
} else if (yearSpan > 1) {
// 1-2年,每季度一个刻度
timeInterval = d3.timeMonth.every(3);
formatFunction = d => {
const month = d.getMonth();
const quarter = Math.floor(month / 3) + 1;
return `${d.getFullYear().toString().slice(-2)}Q${quarter}`;
};
} else if (monthSpan > 6) {
// 6个月-1年,每月一个刻度
timeInterval = d3.timeMonth.every(1);
formatFunction = d => d3.timeFormat("%m %Y")(d);
} else if (monthSpan > 2) {
// 2-6个月,每周一个刻度
timeInterval = d3.timeWeek.every(1);
formatFunction = d => d3.timeFormat("%d %m")(d);
} else {
// 少于2个月,每天一个刻度或每几天一个刻度
const dayInterval = Math.max(1, Math.ceil(daySpan / 10));
timeInterval = d3.timeDay.every(dayInterval);
formatFunction = d => d3.timeFormat("%d %m")(d);
}
// 生成刻度
const xTicks = xScale.ticks(timeInterval);
// 确保包含最后一个日期
if (xTicks.length > 0 && xTicks[xTicks.length - 1] < xExtent[1]) {
if (xTicks.length > 7) {
xTicks.pop(); // 先移除当前最后一个刻度
}
xTicks.push(xExtent[1]); // 添加数据的最后一个日期作为刻度
}
return {
xScale: xScale,
xTicks: xTicks,
xFormat: formatFunction,
timeSpan: {
days: daySpan,
months: monthSpan,
years: yearSpan
}
};
};
var createNumericalFormatter = (data, yField) => {
const yExtent = d3.extent(data, d => d[yField]);
const yRange = yExtent[1] - yExtent[0];
const yPadding = yRange * 0.05;
if (yRange > 1000000000) {
return d => d3.format(".2f")(d / 1000000000) + "B";
} else if (yRange > 1000000) {
return d => d3.format(".2f")(d / 1000000) + "M";
} else if (yRange > 1000) {
return d => d3.format(".2f")(d / 1000) + "K";
} else {
return d => d3.format(".2f")(d);
}
}
/**
* 根据数据点数量计算需要显示标签的点的索引
* @param {number} n - 数据点总数
* @returns {number[]} - 需要显示标签的点的索引数组
*/
var sampleLabels = (n) => {
// 少于10个点时显示所有标签
if (n <= 10) {
return Array.from({length: n}, (_, i) => i);
}
// 超过10个点时每隔 n/10 个点显示一个标签
const step = Math.ceil(n / 10);
const result = [];
// 从0开始,每隔step个点取一个索引
for (let i = 0; i < n; i += step) {
result.push(i);
}
// 确保包含最后一个点
if (result[result.length - 1] !== n - 1) {
result.push(n - 1);
}
return result;
};
var temporalFilter = (data, field) => {
// 把data中不是temporal的点删除
return data.filter(d => {
try {
parseDate(d[field]);
return true;
} catch (e) {
return false;
}
});
}
/**
* 计算文本在指定字体大小下的实际渲染宽度
* @param {string} text - 要测量的文本
* @param {number} fontSize - 字体大小(px)
* @returns {number} - 文本宽度(px)
*/
var getTextWidth = (text, fontSize) => {
return chartUtils.text.measure(null, text, { fontSize }).width;
};
/**
* 智能排版图例,将多个图例元素自动排布成多行
* @param {Object} g - D3 选择的 SVG 组元素,用于放置图例
* @param {Array} groups - 组名数组
* @param {Object} colors - 颜色对象,包含 field 属性
* @param {Object} options - 配置选项
* @param {number} options.maxWidth - 每行最大宽度
* @param {number} options.x - 图例起始 x 坐标
* @param {number} options.y - 图例起始 y 坐标
* @param {number} options.itemHeight - 每个图例项的高度
* @param {number} options.itemSpacing - 图例项之间的水平间距
* @param {number} options.rowSpacing - 行之间的垂直间距
* @param {number} options.symbolSize - 图例符号大小
* @param {string} options.textColor - 文本颜色
* @param {number} options.fontSize - 字体大小
* @param {string} options.fontWeight - 字体粗细
* @param {string} options.align - 对齐方式:'left', 'center', 'right'
* @param {string} options.shape - 图例形状:'circle', 'rect', 'line'
* @returns {Object} 包含图例尺寸信息的对象 {width, height}
*/
var layoutLegend = (g, groups, colors, options = {}) => {
const legendOptions = { ...options };
if (options.symbolSize != null) legendOptions.markerSize = options.symbolSize;
if (options.shape != null) legendOptions.markerShape = options.shape;
if (options.itemSpacing != null) legendOptions.itemGap = options.itemSpacing;
if (options.rowSpacing != null) legendOptions.rowGap = options.rowSpacing;
return chartUtils.legend.draw(g, groups, colors, legendOptions);
};
var formatValue = (value) => {
if (value >= 1000000000) {
return d3.format("~g")(value / 1000000000) + "B";
} else if (value >= 1000000) {
return d3.format("~g")(value / 1000000) + "M";
} else if (value >= 1000) {
return d3.format("~g")(value / 1000) + "K";
} else {
console.log("formatValue", value, d3.format("~g")(value));
return d3.format("~g")(value);
}
}