/** * SVG 图表渲染模块。 * 所有图表都返回字符串,由视图层直接写入 chart-box;交互点通过 data-enterprise/data-project 交给事件代理处理。 */ function renderGroupedRateChart(list, mode, options = {}) { const width = Math.max(1180, list.length * 88 + 120); const height = options.height || 350; const margin = { top: 20, right: 30, bottom: height < 240 ? 58 : 98, left: 54 }; const chartHeight = height - margin.top - margin.bottom; const chartWidth = width - margin.left - margin.right; const groupWidth = chartWidth / Math.max(list.length, 1); const barWidth = Math.min(height < 240 ? 12 : 16, groupWidth / 5); const series = [ { key: "onlineRate", label: "上线", color: COLORS.online }, { key: "usageRate", label: "使用", color: COLORS.usage }, { key: "accuracyRate", label: "准确", color: COLORS.accuracy } ]; const grid = [0, 25, 50, 75, 100].map((tick) => { const y = margin.top + chartHeight - (tick / 100) * chartHeight; return `${tick}%`; }).join(""); const bars = list.map((enterprise, index) => { const stats = enterprise.stats[state.dimension]; const center = margin.left + groupWidth * index + groupWidth / 2; const label = enterprise.name; const rects = series.map((item, seriesIndex) => { const value = stats[item.key]; const barHeight = (value / 100) * chartHeight; const x = center - barWidth * 1.8 + seriesIndex * barWidth * 1.8; const y = margin.top + chartHeight - barHeight; return ` ${Math.round(value)} `; }).join(""); return ` ${rects} ${label} `; }).join(""); return ` ${grid} ${bars} `; } /** * 项目使用率/准确率柱状图。 * 柱子用于进入项目明细,底部圆点表达项目是否已上线,便于在企业层同时查看使用和上线状态。 */ function renderProjectRateChart(projects, options = {}) { const width = Math.max(1100, projects.length * 126 + 120); const height = options.height || 350; const margin = { top: 20, right: 30, bottom: height < 260 ? 66 : 98, left: 54 }; const chartHeight = height - margin.top - margin.bottom; const chartWidth = width - margin.left - margin.right; const groupWidth = chartWidth / Math.max(projects.length, 1); const barWidth = Math.min(20, groupWidth / 4); const series = [ { key: "usageRate", color: COLORS.usage }, { key: "accuracyRate", color: COLORS.accuracy } ]; const grid = [0, 25, 50, 75, 100].map((tick) => { const y = margin.top + chartHeight - (tick / 100) * chartHeight; return `${tick}%`; }).join(""); const bars = projects.map((project, index) => { const center = margin.left + groupWidth * index + groupWidth / 2; const shortName = project.name.replace(project.unit, "").replace("项目", ""); const rects = series.map((item, seriesIndex) => { const value = project[item.key]; const barHeight = (value / 100) * chartHeight; const x = center - barWidth * 1.05 + seriesIndex * barWidth * 2.1; const y = margin.top + chartHeight - barHeight; return ` ${Math.round(value)} `; }).join(""); const statusY = height - (height < 260 ? 48 : 78); const statusColor = project.created ? COLORS.cyan : COLORS.warning; const statusText = project.created ? "已上线" : "未上线"; return ` ${rects} ${statusText} ${shortName} `; }).join(""); return ` ${grid} ${bars} `; } /** * 集团趋势加权平均。 * 按企业项目数加权汇总各企业七日趋势,避免项目少的企业对集团曲线影响过大。 */ function averageTrend(list, key) { const active = list.filter((enterprise) => enterprise.stats[state.dimension].projectCount > 0); return Array.from({ length: 7 }, (_, index) => { if (!active.length) return 0; const weighted = active.reduce((sum, enterprise) => { const projects = enterprise.stats[state.dimension].projectCount; return sum + enterprise.trend[key][index] * projects; }, 0); const total = active.reduce((sum, enterprise) => sum + enterprise.stats[state.dimension].projectCount, 0); return weighted / total; }); } /** * 集团三率趋势图。 * 先把企业趋势压成集团趋势,再复用通用折线图渲染方法。 */ function renderGroupTrendChart(list) { return renderTrendChart({ online: averageTrend(list, "online"), usage: averageTrend(list, "usage"), accuracy: averageTrend(list, "accuracy") }, 1840, 112); } /** * 通用三率折线图。 * data 需要包含 online/usage/accuracy 三组七日数组,适用于集团趋势和企业趋势。 */ function renderTrendChart(data, width = 720, height = 250) { const margin = { top: 14, right: 28, bottom: 28, left: 46 }; const chartWidth = width - margin.left - margin.right; const chartHeight = height - margin.top - margin.bottom; const dates = ["04-21", "04-22", "04-23", "04-24", "04-25", "04-26", "04-27"]; const series = [ { key: "online", label: "上线", color: COLORS.online }, { key: "usage", label: "使用", color: COLORS.usage }, { key: "accuracy", label: "准确", color: COLORS.accuracy } ]; const grid = [0, 25, 50, 75, 100].map((tick) => { const y = margin.top + chartHeight - (tick / 100) * chartHeight; return `${tick}%`; }).join(""); const lines = series.map((item) => { const values = data[item.key] || []; const points = values.map((value, index) => { const x = margin.left + (chartWidth / (values.length - 1)) * index; const y = margin.top + chartHeight - (value / 100) * chartHeight; return `${x},${y}`; }).join(" "); const circles = values.map((value, index) => { const x = margin.left + (chartWidth / (values.length - 1)) * index; const y = margin.top + chartHeight - (value / 100) * chartHeight; return ``; }).join(""); return `${circles}`; }).join(""); const labels = dates.map((date, index) => { const x = margin.left + (chartWidth / (dates.length - 1)) * index; return `${date}`; }).join(""); return ` ${grid} ${lines} ${labels} `; }