Search code examples
javascriptd3.jsobservableradial

How to convert Observable radial chart code to plain Javascript?


I am trying to convert observable notebook (https://observablehq.com/@d3/radial-area-chart) to vanilla Javascript using d3 SVG elements. You can find the 'sfo-temperature.csv' by clicking on paper clip symbol on the link above.

I am still beginner in HTML, JS and D3. I am stuck in an error. I would appreciate any alternative solutions as well. Below, you can find my attempt.

Here is my index.HTML file:

<html>
  <head>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <script src="d3Chart.js"></script>
    <!-- <link rel="stylesheet" type="text/css" href="styles.css"> -->
  </head>
  <body>
    <div id="chart-container"></div>
    <div class="container"></div>
    <script>

            // Call the drawChart function with the container and data
      const container = d3.select("#chart-container");

      // Upload local CSV file
      d3.csv("sfo-temperature.csv").then(function(data) {
        drawChart('.container', data);
      });
      <!--drawChart('.container',data)-->
    </script>
  </body>
</html>

Here is my d3Chart.js file:

async function drawChart(container, data) {
    const rawdata = await d3.csv("sfo-temperature.csv");
    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]);

    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, i) => d.id = DOM.uid("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("xlink:href", d => d.id.href)
                .text(d3.utcFormat("%B"))));

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

    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)
        .selectAll("path")
        .data(data)
        .join("path")
        .style("mix-blend-mode", "multiply")
        .attr("stroke", "steelblue")
        .attr("d", d => line(d.values));

    svg.append("g")
        .call(xAxis);

    svg.append("g")
        .call(yAxis);

    svg.append("g")
        .selectAll("g")
        .data(data)
        .join("g")
        .attr("transform", d => `
          rotate(${((x(d.date) + x(d3.utcMonth.offset(d.date, 1))) / 2 * 180 / Math.PI - 90)})
          translate(${innerRadius},0)
        `)
        .append("line")
        .attr("x2", -5)
        .attr("stroke", "#000");

    svg.append("g")
        .selectAll("g")
        .data(data)
        .join("g")
        .attr("transform", d => `
          rotate(${((x(d.date) + x(d3.utcMonth.offset(d.date, 1))) / 2 * 180 / Math.PI - 90)})
          translate(${outerRadius},0)
        `)
        .append("line")
        .attr("x2", 5)
        .attr("stroke", "#000");
}

When I run my code, I encounter the following error: Uncaught (in promise) TypeError: d.DATE.getUTCMonth is not a function I tried replacing d.DATE.getUTCMonth with d.getUTCMonth, however, it still did not work. How should I modify my code so that I can create the radial area chart deined on obsrvable run using vanilla JS and d3 SVG elements?


Solution

  • Instead of using asynchronous function, I changed it with d3.csv function to read the CSV file and define rollup on it.

    
        const width = 954
        const height = width
        const margin = 10
        const innerRadius = width / 5
        const outerRadius = width / 2 - margin
    
        // ++++++++++++++++++++++++++++++++++++++++ Step 1) Parsing the CSV file ++++++++++++++++++++++++++++
        d3.csv("sfo-temperature.csv", function(d) {
          return {
            // Parse the CSV data and return a JavaScript object
            // with the desired properties
            DATE: new Date(d.DATE),
            TAVG: +d.TAVG,
            TMAX: +d.TMAX,
            TMIN: +d.TMIN,
          };
        }).then(function(rawdata) {
          // ++++++++++++++++++++++++++++++++++++++++ Step 2) GROUPBY ++++++++++++++++++++++++++++
          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 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])
    

    Note that for xAxis variable I replaced d.id = DOM.uid("month") with d.id = month-${i}, since DOM.uid is not defined outside of the Observable environment. Also, I replaced xlink:href with href because the former is deprecated in SVG 2.0. Finally, I added a function call to d3.utcFormat("%B") to format the month names.

    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, i) => d.id = `month-${i}`)
          .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)
              .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}`)
              .text((d) => d3.utcFormat("%B")(d)))
      );
    
    
    
        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((x, i) => `${x.toFixed(0)}${i ? "" : "°F"}`)
              .clone(true)
                .attr("y", d => y(d))
              .selectAll(function() { return [this, this.previousSibling]; })
              .clone(true)
                .attr("fill", "currentColor")
                .attr("stroke", "none")))
    
    
        const line = d3.lineRadial()
        .curve(d3.curveLinearClosed)
        .angle(d => x(d.date))
    
        const area = d3.areaRadial()
        .curve(d3.curveLinearClosed)
        .angle(d => x(d.date))
    
    
      const svg = d3.create("svg")
          .attr("viewBox", [-width / 2, -height / 2, width, height])
          .attr("stroke-linejoin", "round")
          .attr("stroke-linecap", "round");
    
      svg.append("path")
          .attr("fill", "lightsteelblue")
          .attr("fill-opacity", 0.2)
          .attr("d", area
              .innerRadius(d => y(d.minmin))
              .outerRadius(d => y(d.maxmax))
            (data));
    
      svg.append("path")
          .attr("fill", "steelblue")
          .attr("fill-opacity", 0.2)
          .attr("d", area
              .innerRadius(d => y(d.min))
              .outerRadius(d => y(d.max))
            (data));
    
      svg.append("path")
          .attr("fill", "none")
          .attr("stroke", "steelblue")
          .attr("stroke-width", 1.5)
          .attr("d", line
              .radius(d => y(d.avg))
            (data));
    
      svg.append("g")
          .call(xAxis);
    
      svg.append("g")
          .call(yAxis);
    
    const container = d3.select("body").append("div");
    container.node().appendChild(svg.node());
          // console.log(xAxis())
        });