(function() { const dom = document.getElementById('chart-convex-hull'); const width = dom.clientWidth || 800; const height = 450; const margin = { top: 20, right: 30, bottom: 40, left: 50 }; const innerW = width - margin.left - margin.right; const innerH = height - margin.top - margin.bottom; // 两组数据 const groupA = [[31, 65], [45, 55], [22, 48], [58, 72], [15, 35], [38, 60], [52, 45], [28, 58]]; const groupB = [[12, 25], [18, 32], [8, 20], [25, 28], [15, 18], [22, 35], [10, 22], [28, 30]]; const allPoints = [...groupA, ...groupB]; const xExtent = d3.extent(allPoints, d => d[0]); const yExtent = d3.extent(allPoints, d => d[1]); const xScale = d3.scaleLinear().domain([xExtent[0] - 5, xExtent[1] + 5]).range([0, innerW]); const yScale = d3.scaleLinear().domain([yExtent[1] + 5, yExtent[0] - 5]).range([0, innerH]); const color = d3.scaleOrdinal(['#3b82f6', '#ef4444']); const svg = d3.select(dom).append('svg').attr('width', width).attr('height', height); const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`); g.append('g').call(d3.axisBottom(xScale)).attr('transform', `translate(0,${innerH})`); g.append('g').call(d3.axisLeft(yScale)); // Graham Scan 凸包算法 function convexHull(points) { points = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); if (points.length <= 2) return points; const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); const lower = []; for (const p of points) { while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop(); lower.push(p); } const upper = []; for (const p of points.reverse()) { while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop(); upper.push(p); } upper.pop(); lower.pop(); return lower.concat(upper); } [groupA, groupB].forEach((group, idx) => { // 散点 g.selectAll(`.dot-g${idx}`).data(group).join('circle') .attr('cx', d => xScale(d[0])).attr('cy', d => yScale(d[1])) .attr('r', 5).attr('fill', color(idx)).attr('opacity', 0.7); // 凸包 const hull = convexHull(group); const lineGen = d3.line().x(d => xScale(d[0])).y(d => yScale(d[1])); g.append('path').datum(hull) .attr('d', lineGen).attr('fill', color(idx)).attr('fill-opacity', 0.1) .attr('stroke', color(idx)).attr('stroke-width', 1.5).attr('stroke-dasharray', '4,2'); }); window.addEventListener('resize', () => { const w = dom.clientWidth || 800; svg.attr('width', w); xScale.range([0, w - margin.left - margin.right]); }); })();