0

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?

dsapprentice
  • 114
  • 12
  • Your `d.DATE` object is probably not a `Date` object, you might be able to transform it to such by calling `new Date(d.DATE)`. – Nils Kähler Apr 17 '23 at 13:54
  • @NilsKähler I changed that line as you mentioned. Now ` d => `${d3.utcParse("%Y-%m-%d")(d.date).getUTCMonth()}-${d3.utcParse("%Y-%m-%d")(d.date).getUTCDate()}` ` raises Uncaught (in promise) TypeError: d3.utcParse(...)(...) is null error – dsapprentice Apr 17 '23 at 14:07
  • It's hard to determine with the information I have, but your `d.date`, might be null? Did you mean `d.DATE`? – Nils Kähler Apr 17 '23 at 14:16
  • When I write d.DATE, then I encounter *ReferenceError: d is not defined* for that specific line we talk about. – dsapprentice Apr 17 '23 at 14:20

1 Answers1

0

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

dsapprentice
  • 114
  • 12