// 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); } }