You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将Observable径向面积图代码转为原生JS并修复DATE相关报错

解决D3径向面积图转换中的日期处理错误及完整实现方案

错误原因

报错d.DATE.getUTCMonth is not a function是因为D3.csv默认将CSV中的DATE字段解析为字符串,而非Date对象,无法调用日期方法。Observable Notebook会自动处理日期解析,但原生JS环境需要手动转换。

关键修改步骤

  1. 解析CSV时转换日期类型:在读取CSV时,通过行转换函数将DATE字段转为Date对象。
  2. 移除重复的CSV读取逻辑:drawChart函数已通过参数接收数据,无需再次调用d3.csv。
  3. 替换Observable专属API:原生环境没有DOM.uid,改用自定义函数生成唯一ID。
  4. 修复线条绘制逻辑:原代码错误地将单个数据点作为路径数据源,应传入完整数据集生成单条径向线。

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 00:27:02