如何将Observable径向面积图代码转为原生JS并修复DATE相关报错
解决D3径向面积图转换中的日期处理错误及完整实现方案
错误原因
报错d.DATE.getUTCMonth is not a function是因为D3.csv默认将CSV中的DATE字段解析为字符串,而非Date对象,无法调用日期方法。Observable Notebook会自动处理日期解析,但原生JS环境需要手动转换。
关键修改步骤
- 解析CSV时转换日期类型:在读取CSV时,通过行转换函数将
DATE字段转为Date对象。 - 移除重复的CSV读取逻辑:
drawChart函数已通过参数接收数据,无需再次调用d3.csv。 - 替换Observable专属API:原生环境没有
DOM.uid,改用自定义函数生成唯一ID。 - 修复线条绘制逻辑:原代码错误地将单个数据点作为路径数据源,应传入完整数据集生成单条径向线。
修改后的完整代码
index.html
<html> <head> <script src="https://d3js.org/d3.v6.min.js"></script> <script src="d3Chart.js"></script> </head> <body> <div class="container"></div> <script> // 读取CSV并转换日期、数值字段类型 d3.csv("sfo-temperature.csv", d => ({ DATE: new Date(d.DATE), TAVG: +d.TAVG, TMIN: +d.TMIN, TMAX: +d.TMAX })).then(function(data) { drawChart('.container', data); }); </script> </body> </html>
d3Chart.js
function drawChart(container, rawdata) { // 按"月-日"分组,聚合每日温度统计值 const data = Array.from(d3.rollup( rawdata, v => ({ date: new Date(Date.UTC(2000, v[0].DATE.getUTCMonth(), v[0].DATE.getUTCDate())), avg: d3.mean(v, d => d.TAVG || NaN), min: d3.mean(v, d => d.TMIN || NaN), max: d3.mean(v, d => d.TMAX || NaN), minmin: d3.min(v, d => d.TMIN || NaN), maxmax: d3.max(v, d => d.TMAX || NaN) }), d => `${d.DATE.getUTCMonth()}-${d.DATE.getUTCDate()}` ).values()) .sort((a, b) => d3.ascending(a.date, b.date)); const width = 954; const height = width; const margin = 10; const innerRadius = width / 5; const outerRadius = width / 2 - margin; // 角度比例尺:映射日期到圆周角度 const x = d3.scaleUtc() .domain([Date.UTC(2000, 0, 1), Date.UTC(2001, 0, 1) - 1]) .range([0, 2 * Math.PI]); // 半径比例尺:映射温度值到径向距离 const y = d3.scaleLinear() .domain([d3.min(data, d => d.minmin), d3.max(data, d => d.maxmax)]) .range([innerRadius, outerRadius]); // 自定义生成唯一ID,替代Observable的DOM.uid function generateUid(prefix) { const id = `${prefix}-${Math.random().toString(36).slice(2, 10)}`; return { id, href: `#${id}` }; } // X轴(径向月份刻度) const xAxis = g => g .attr("font-family", "sans-serif") .attr("font-size", 10) .call(g => g.selectAll("g") .data(x.ticks()) .join("g") .each((d) => d.id = generateUid("month")) .call(g => g.append("path") .attr("stroke", "#000") .attr("stroke-opacity", 0.2) .attr("d", d => ` M${d3.pointRadial(x(d), innerRadius)} L${d3.pointRadial(x(d), outerRadius)} `)) .call(g => g.append("path") .attr("id", d => d.id.id) .datum(d => [d, d3.utcMonth.offset(d, 1)]) .attr("fill", "none") .attr("d", ([a, b]) => ` M${d3.pointRadial(x(a), innerRadius)} A${innerRadius},${innerRadius} 0,0,1 ${d3.pointRadial(x(b), innerRadius)} `)) .call(g => g.append("text") .append("textPath") .attr("startOffset", 6) .attr("href", d => d.id.href) .text(d3.utcFormat("%B")))); // Y轴(径向温度刻度) const yAxis = g => g .attr("text-anchor", "middle") .attr("font-family", "sans-serif") .attr("font-size", 10) .call(g => g.selectAll("g") .data(y.ticks().reverse()) .join("g") .attr("fill", "none") .call(g => g.append("circle") .attr("stroke", "#000") .attr("stroke-opacity", 0.2) .attr("r", y)) .call(g => g.append("text") .attr("y", d => -y(d)) .attr("dy", "0.35em") .attr("stroke", "#fff") .attr("stroke-width", 5) .text(y.tickFormat(5, "f"))) .call(g => g.append("text") .attr("y", d => -y(d)) .attr("dy", "0.35em") .text(y.tickFormat(5, "f")))); // 径向线生成器,添加闭合曲线 const line = d3.lineRadial() .angle(d => x(d.date)) .radius(d => y(d.avg)) .curve(d3.curveLinearClosed); // 创建SVG容器 const svg = d3.select(container) .append("svg") .attr("viewBox", [-width / 2, -height / 2, width, height]) .attr("font-family", "sans-serif") .attr("font-size", 12) .attr("text-anchor", "middle"); // 绘制平均温度径向线 svg.append("g") .attr("fill", "none") .attr("stroke-opacity", 0.6) .append("path") .style("mix-blend-mode", "multiply") .attr("stroke", "steelblue") .attr("stroke-width", 2) .attr("d", line(data)); // 添加X轴和Y轴 svg.append("g").call(xAxis); svg.append("g").call(yAxis); // 绘制月份刻度内外短线 svg.append("g") .selectAll("g") .data(x.ticks()) .join("g") .attr("transform", d => ` rotate(${((x(d) + x(d3.utcMonth.offset(d, 1))) / 2 * 180 / Math.PI - 90)}) translate(${innerRadius},0) `) .append("line") .attr("x2", -5) .attr("stroke", "#000"); svg.append("g") .selectAll("g") .data(x.ticks()) .join("g") .attr("transform", d => ` rotate(${((x(d) + x(d3.utcMonth.offset(d, 1))) / 2 * 180 / Math.PI - 90)}) translate(${outerRadius},0) `) .append("line") .attr("x2", 5) .attr("stroke", "#000"); }
额外说明
- 添加
curveLinearClosed使径向线闭合,贴合原Observable图表效果。 - CSV读取时同步转换温度字段为数值类型,避免后续计算出错。
- 移除冗余DOM元素,简化页面结构。
内容的提问来源于stack exchange,提问作者dsapprentice
相关产品推荐
相关产品推荐

