skip to Main Content

I’m attempting to display a basic animated bar-chart using D3 (v7) to my web page. This is being done from within a Django project and so my chart is being fed data from my views.py file. However, the chart only partially displays on the page. The x and y axis lines are displayed, along with the axis labels. Here is my error message:

Error: <rect> attribute height: Expected length, "NaN".

Here is the relevant part of my html template:

 <div id="chart-container"></div>

        <script src="https://d3js.org/d3.v7.min.js"></script>

        <script type="text/javascript">
          const data = JSON.parse('{{ assign_levels|safe }}');

          const margin = {top: 10, right: 30, bottom: 140, left: 60};
          const width = 400 - margin.left - margin.right;
          const height = 500 - margin.top - margin.bottom;

          const svg = d3.select("#chart-container")
              .append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", `translate(${margin.left}, ${margin.top})`);

          const x = d3.scaleBand()
              .range([0, width])
              .domain(data.map(d => d.level_assign))
              .padding(0.3);
          
          const y = d3.scaleLinear()
              .range([height, 0])
              .domain([0, d3.max(data, d => d.count)]);

          svg.append("g")
              .call(d3.axisLeft(y));

          svg.selectAll(".bar")
              .data(data)
              .enter()
              .append("rect")
              .attr("class", "bar")
              .attr("x", d => x(d.level_assign))
              .attr("width", x.bandwidth())
              .attr("y", height)
              .attr("height", 0)
              .attr("fill", "#5F9EA0")
              .transition()
              .duration(1000)
              .delay((d, i) => i * 100)
              .attr("y", d => y(d.count))
              .attr("height", d => height - y(d.count));

          svg.append("g")
              .attr("transform", `translate(0, ${height})`)
              .call(d3.axisBottom(x))
              .selectAll("text")
              .attr("transform", "rotate(-45)")
              .style("text-anchor", "end")
              .attr("dx", "-.8em")
              .attr("dy", ".15em");

          svg.append("text")
              .attr("transform", "rotate(-90)")
              .attr("y", 0 - margin.left)
              .attr("x",0 - (height / 2))
              .attr("dy", "1em")
              .style("text-anchor", "middle")
              .text("Study Count");

          svg.append("text")
              .attr("x", (width + margin.left + margin.right) / 2)
              .attr("y", margin.top / 2)
              .attr("text-anchor", "middle")
              .style("font-size", "16px")
              .text("Level of Assignment");
      </script>

And here is the code I am inputting to the chart:

[["Class", 36], ["School - cluster", 6], ["Individual", 20], ["N", 1], ["School - multi-site", 9], ["Not provided/ not available", 4], ["Region or district", 1]]

I can’t understand why the error message is telling me that it has received a "NaN" for height as I can see that the height has been set in absolute terms e.g. 500.

2

Answers


  1. The Python pandas dataframe returns NaN for empty results (short for “Not a Number”). You can change it by using the fillna method on your dataframe:

    df = df.fillna('')

    This should solve the problem downstream for display issues, but you’ll have to dig a bit deeper to find why it is being passed for height.

    Login or Signup to reply.
  2. You’re passing an array of arrays as data but the D3 code expects an array of objects with specific keys.
    I tried to convert the data format into an array of objects.

    You need to update your code slightly to achieve this. Check the code below:

    const rawData = JSON.parse('{{ assign_levels|safe }}');
    const data = rawData.map(d => ({level_assign: d[0], count: d[1]}));
    

    This will convert your data to this format instead of a list of arrays.

    [
      {"level_assign": "Class", "count": 36},
      {"level_assign": "School - cluster", "count": 6},
      {"level_assign": "Individual", "count": 20},
      {"level_assign": "N", "count": 1},
      {"level_assign": "School - multi-site", "count": 9},
      {"level_assign": "Not provided/ not available", "count": 4},
      {"level_assign": "Region or district", "count": 1}
    ]
    

    Here is an example with full code:

    //script.js
    const rawData = [
        ["Class", 36],
        ["School - cluster", 6],
        ["Individual", 20],
        ["N", 1],
        ["School - multi-site", 9],
        ["Not provided/ not available", 4],
        ["Region or district", 1],
    ];
    
    const data = rawData.map((d) => ({
        level_assign: d[0],
        count: d[1]
    }));
    
    const margin = {
        top: 10,
        right: 30,
        bottom: 140,
        left: 60
    };
    const width = 400 - margin.left - margin.right;
    const height = 500 - margin.top - margin.bottom;
    
    const svg = d3
        .select("#chart-container")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", `translate(${margin.left}, ${margin.top})`);
    
    const x = d3
        .scaleBand()
        .range([0, width])
        .domain(data.map((d) => d.level_assign))
        .padding(0.3);
    
    const y = d3
        .scaleLinear()
        .range([height, 0])
        .domain([0, d3.max(data, (d) => d.count)]);
    
    svg.append("g").call(d3.axisLeft(y));
    
    svg.selectAll(".bar")
        .data(data)
        .enter()
        .append("rect")
        .attr("class", "bar")
        .attr("x", (d) => x(d.level_assign))
        .attr("width", x.bandwidth())
        .attr("y", height)
        .attr("height", 0)
        .attr("fill", "#5F9EA0")
        .transition()
        .duration(1000)
        .delay((d, i) => i * 100)
        .attr("y", (d) => y(d.count))
        .attr("height", (d) => height - y(d.count));
    
    svg.append("g")
        .attr("transform", `translate(0, ${height})`)
        .call(d3.axisBottom(x))
        .selectAll("text")
        .attr("transform", "rotate(-45)")
        .style("text-anchor", "end")
        .attr("dx", "-.8em")
        .attr("dy", ".15em");
    
    svg.append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 0 - margin.left)
        .attr("x", 0 - (height / 2))
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text("Study Count");
    
    svg.append("text")
        .attr("x", (width + margin.left + margin.right) / 2)
        .attr("y", margin.top / 2)
        .attr("text-anchor", "middle")
        .style("font-size", "16px")
        .text("Level of Assignment");
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>D3 Bar Chart</title>
    </head>
    <body>
        <div id="chart-container"></div>
    
        <script src="https://d3js.org/d3.v7.min.js"></script>
        <script src="script.js"></script>
    </body>
    </html>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search