skip to Main Content

This is a continuation of [this][1] question so right now I can get the data and it displays how many rounds a user has played and all their scores but as for the horizontal bar chart it only displays the bar chart in the first iteration in the thymeleaf th:each. So [Wim Deblauwe][2] was nice enough to tell me I needed to use a javascript fetch() method and direct me to his website and a 40min lecture he gave about htmx. But that is all still beyond me.

So my question, why isn’t the (chartjs) bar chart able to be displayed in a loop? Why does it differ from displaying text? When I use javascript on a datepicker the th:each works fine.

So in my example how can I get a horizontal bar in each loop of the <th:block th:each="round : ${roundCourse.value}">? Again Im a beginner with javascript and relatively new with spring/java/thymeleaf, the below code shows the extent of my knowledge, there is screen shots and more info in the link above. Thanks in advance.

Rounds.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link th:href="@{/css/fontawesome/css/all.css}" rel="stylesheet">
    <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap/bootstrap.min.css}"/>
    <link rel="stylesheet" th:href="@{https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css}">
    <link rel="stylesheet" type="text/css" th:href="@{/css/discgolf/round.css}">

    <title>Rounds</title>


</head>
<body>
<div th:replace="fragments/navbars/libraryNavbar :: navbar"></div>
<div class="container">
    <div class="subContainer">
        <h1>Rounds</h1>
        <a class="col-sm-4" th:href="@{/discgolf}">Disc Golf Home</a>
        <div class="row" id="username">
            <b class="col-sm-8">Username:<span sec:authentication="principal.username"></span></b>
        </div>
    </div>
    <a>Add Round</a>
    <form action="#" th:action="@{/discgolf/newRound}" th:object="${course}"
          method="GET">
        <div class="form-group">
            <div class="form-group blu-margin">
                <select th:name="course" class="form-control" onchange="this.form.submit()">
                    <option th:value="0" th:text="${'Please Select'}"></option>
                    <option th:each="course : ${courses}"
                            th:text="${course.name}" >

                    </option>
                </select>
            </div>
        </div>
    </form>
</div>
<div class="container">
    <div>
        <a>Rounds Played</a>
    </div>
    <div >
        <div th:each="roundCourse : ${rounds}" class="card">
            <button class="accordion">
                <span th:text="${roundCourse.key.name}"></span>
            </button>
            <div class="panel">
                <div class="row">
                    <div class="col-3">
                        <label>Record: </label>
                        <label th:if="${roundCourse.key.record > 0}" th:text="${'+' + roundCourse.key.record + ' (' + (roundCourse.key.par + roundCourse.key.record) + ')'}"></label>
                        <label th:if="${roundCourse.key.record < 0}" th:text="${roundCourse.key.record + ' (' + (roundCourse.key.record + roundCourse.key.par) + ')'}"></label>
                        <label th:if="${roundCourse.key.record == 0}" th:text="${'E (' + (roundCourse.key.record + roundCourse.key.par) + ')'}"></label>
                    </div>
                    <div class="col-3">
                        <label>Times played: </label>
                        <label th:text="${#lists.size(roundCourse.value)}"></label>
                    </div>
                    <div>
                        <label>My best:</label>
                        <label th:if="${(roundService.getBestRoundScoreByCourseId(userId, roundCourse.key.id) - roundCourse.key.par) == 0}" th:text="${'E'}"></label>
                        <label th:if="${(roundService.getBestRoundScoreByCourseId(userId, roundCourse.key.id) - roundCourse.key.par) < 0}"
                               th:text="${roundService.getBestRoundScoreByCourseId(userId, roundCourse.key.id) - roundCourse.key.par}"></label>
                        <label th:text="${'(' + roundService.getBestRoundScoreByCourseId(userId, roundCourse.key.id) + ')'}"></label>
                    </div>
                </div>
                <hr>
           ***Here, bar chart is only displayed the first iteration***
                <th:block th:each="round : ${roundCourse.value}">
                <div class="card-body">
                    <div class="row">
                        <div class="col-3">
                            <label>Date: </label>
                            <label th:text="${#dates.format(round.roundDate, 'dd-MMM-yyyy')}"></label>
                        </div>
                        <div class="col-3">
                            <label>Score: </label>
                            <label th:if="${round.total - round.course.par == 0}" th:text="${'E'}"></label>
                            <label th:if="${round.total - round.course.par > 0}" th:text="${'+' + (round.total - round.course.par)}"></label>
                            <label th:text="${'(' + round.total + ')'}"></label>
                        </div>
                        <div class="col-6">
                            <div class="container-fluid">
                                <canvas th:attr="data-counts=${roundService.getListOfScoresByRoundId(round.roundId)}" id="myChart"></canvas>
<!--                                <canvas th:attr="data-counts=${roundService.getListOfScoresByRoundId(round.roundId)}" th:id="'myChart' + ${round.roundId}"></canvas>-->
                            </div>
                        </div>
                    </div>
                    <br>
                    <div >
                        <table id="courseInfo" class="table table-bordered w-auto">
                            <th:block th:each="course : ${round.course}">
                                <tr>
                                    <th th:text="${'Hole'}"></th>
                                    <th th:each="hole : ${course.holes}" th:text="${hole.number}"></th>
                                    <th th:text="${'Total'}"></th>
                                </tr>
                                <tr>
                                    <td th:text="${'Par'}"></td>
                                    <td th:each="par : ${course.holes}" th:text="${par.par}"></td>
                                    <td th:text="${course.par}"></td>
                                </tr>
                                <tr>
                                    <td th:text="${'Score'}"></td>
                                    <th:block th:each="score : ${round.scores}">
                                        <td  th:style="${score.score > score.holePar} ? 'background-color: #FDD79C'
                                                    : (${score.score < score.holePar } ? 'background-color: #77ACD8'
                                                    : 'background-color: #eee' ) "
                                            th:text="${score.score}">

                                        </td>
                                    </th:block>
                                    <td th:text="${round.total}"></td>
                                </tr>
                            </th:block>
                        </table>
                        <br>
                        <a th:href="@{/discgolf/deleteRound/{id}(id=${round.roundId})}" title="Remove Course"
                           data-target="#deleteRoundModal" class="table-link danger" id="deleteRoundButton" >
                            <span id="deleteRound" class="fa-stack">
                                <i class="fa fa-square fa-stack-2x"></i>
                                <i class="fa fa-trash-o fa-stack-1x fa-inverse" title="Delete this round"></i>
                            </span>
                        </a>
                    </div>
                </div>
                <hr>
                </th:block>
            </div>
        </div>
    </div>
</div>
<script th:inline="javascript">
    var listRounds = [[${rounds}]];

</script>

<script type="text/javascript" src="/js/jquery-3.6.0.js"></script>
<script type="text/javascript" src="/js/bootstrap/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<script th:src="@{/js/discgolf/userRounds.js}"></script>
</body>
</html>

userRounds.js

const countsTest = document.getElementById('myChart').getAttribute('data-counts');
const counts = {};

for (const num of countsTest) {
  counts[num] = counts[num] ? counts[num] + 1 : 1;
}

var acc = document.getElementsByClassName("accordion");
var i;

for (i = 0; i < acc.length; i++) {
  acc[i].addEventListener("click", function() {
    this.classList.toggle("active");
    var panel = this.nextElementSibling;
    if (panel.style.maxHeight) {
      panel.style.maxHeight = null;
    } else {
      panel.style.maxHeight = panel.scrollHeight + "px";
    }
  });
}


  new Chart(document.getElementById('myChart'),{
      type: 'bar',
      options: {
        responsive: true,
        maintainAspectRatio: false,
        indexAxis: 'y',
        scales: {
          x: {
            stacked: true,
            display: false
          },
          y: {
            stacked: true,
            display: false
          }
        },
        plugins: {
          legend: {
            display: false
          }
        },
      },

      data: {
        labels: ["Score"],

        datasets: [{
          data: [counts[2]],
          backgroundColor: "#77ACD8"
        },{
          data: [counts[3]]
        },{
          data: [counts[4]],
          backgroundColor: "#FDD79C"
        },{
           data: [counts[5]],
           backgroundColor: "#FDC26A"
         },{
             data: [counts[6], counts[7], counts[8], counts[9], counts[10]],
             backgroundColor: "#FCAE37"
           }]
      }
    }
  );

Data

Course{id=2, name='Ilsede', holes=[Hole{holeId=46, number=1, par=3}, Hole{holeId=47, number=2, par=3}, Hole{holeId=48, number=3, par=3}, Hole{holeId=49, number=4, par=3}, Hole{holeId=50, number=5, par=3}, Hole{holeId=51, number=6, par=3}, Hole{holeId=52, number=7, par=3}, Hole{holeId=53, number=8, par=3}, Hole{holeId=54, number=9, par=3}, Hole{holeId=55, number=10, par=3}, Hole{holeId=56, number=11, par=3}, Hole{holeId=57, number=12, par=3}, Hole{holeId=58, number=13, par=4}, Hole{holeId=59, number=14, par=3}, Hole{holeId=60, number=15, par=3}, Hole{holeId=61, number=16, par=3}, Hole{holeId=62, number=17, par=3}, Hole{holeId=63, number=18, par=3}], par=55, record=7}
        =[Round{roundId=21, course=Course{id=2, name='Ilsede', holes=[Hole{holeId=46, number=1, par=3}, Hole{holeId=47, number=2, par=3}, Hole{holeId=48, number=3, par=3}, Hole{holeId=49, number=4, par=3}, Hole{holeId=50, number=5, par=3}, Hole{holeId=51, number=6, par=3}, Hole{holeId=52, number=7, par=3}, Hole{holeId=53, number=8, par=3}, Hole{holeId=54, number=9, par=3}, Hole{holeId=55, number=10, par=3}, Hole{holeId=56, number=11, par=3}, Hole{holeId=57, number=12, par=3}, Hole{holeId=58, number=13, par=4}, Hole{holeId=59, number=14, par=3}, Hole{holeId=60, number=15, par=3}, Hole{holeId=61, number=16, par=3}, Hole{holeId=62, number=17, par=3}, Hole{holeId=63, number=18, par=3}], par=55, record=7}, scores=[Score{scoreId=199, score=3, holePar=3}, Score{scoreId=200, score=3, holePar=3}, Score{scoreId=201, score=3, holePar=3}, Score{scoreId=202, score=4, holePar=3}, Score{scoreId=203, score=3, holePar=3}, Score{scoreId=204, score=3, holePar=3}, Score{scoreId=205, score=2, holePar=3}, Score{scoreId=206, score=3, holePar=3}, Score{scoreId=207, score=3, holePar=3}, Score{scoreId=208, score=4, holePar=3}, Score{scoreId=209, score=3, holePar=3}, Score{scoreId=210, score=3, holePar=3}, Score{scoreId=211, score=2, holePar=3}, Score{scoreId=212, score=3, holePar=3}, Score{scoreId=213, score=3, holePar=3}, Score{scoreId=214, score=4, holePar=3}, Score{scoreId=215, score=3, holePar=3}, Score{scoreId=216, score=2, holePar=3}], roundDate=2023-03-01 00:00:00.0, total=54},
        Round{roundId=24, course=Course{id=2, name='Ilsede', holes=[Hole{holeId=46, number=1, par=3}, Hole{holeId=47, number=2, par=3}, Hole{holeId=48, number=3, par=3}, Hole{holeId=49, number=4, par=3}, Hole{holeId=50, number=5, par=3}, Hole{holeId=51, number=6, par=3}, Hole{holeId=52, number=7, par=3}, Hole{holeId=53, number=8, par=3}, Hole{holeId=54, number=9, par=3}, Hole{holeId=55, number=10, par=3}, Hole{holeId=56, number=11, par=3}, Hole{holeId=57, number=12, par=3}, Hole{holeId=58, number=13, par=4}, Hole{holeId=59, number=14, par=3}, Hole{holeId=60, number=15, par=3}, Hole{holeId=61, number=16, par=3}, Hole{holeId=62, number=17, par=3}, Hole{holeId=63, number=18, par=3}], par=55, record=7}, scores=[Score{scoreId=244, score=3, holePar=3}, Score{scoreId=245, score=3, holePar=3}, Score{scoreId=246, score=3, holePar=3}, Score{scoreId=247, score=3, holePar=3}, Score{scoreId=248, score=4, holePar=3}, Score{scoreId=249, score=3, holePar=3}, Score{scoreId=250, score=3, holePar=3}, Score{scoreId=251, score=3, holePar=3}, Score{scoreId=252, score=2, holePar=3}, Score{scoreId=253, score=3, holePar=3}, Score{scoreId=254, score=3, holePar=3}, Score{scoreId=255, score=3, holePar=3}, Score{scoreId=256, score=2, holePar=3}, Score{scoreId=257, score=3, holePar=3}, Score{scoreId=258, score=3, holePar=3}, Score{scoreId=259, score=4, holePar=3}, Score{scoreId=260, score=3, holePar=3}, Score{scoreId=261, score=3, holePar=3}], roundDate=2023-03-09 00:00:00.0, total=54}]

Controller

@GetMapping("/rounds/{id}")
    public String roundsHome(@PathVariable(value = "id") Long id,
                             Model model) {
        List<Course> courses = courseService.getAllCourses();
        List<Round> rounds = userService.getUserById(id).getRounds();
        Map<Course, List<Round>> mapRoundsByCourse = rounds.stream().collect(Collectors.groupingBy(Round::getCourse));
        model.addAttribute("courses", courses);
        model.addAttribute("rounds", mapRoundsByCourse);
        return "/discgolf/round/rounds";
    }
[![enter image description here][3]][3]

UPDATE

So Ive create (I guess) a DTO CourseByRound object that looks like this:

private Long courseId;
    private String courseName;
    private int coursePar;
    private int courseRecord;
    private double courseAverage;
    private int timesPlayed;

    private List<Round> rounds;

//constructor, getters and setters
    }

New chart and accordion:

var acc = document.getElementsByClassName("accordion");
    var i;

    for (i = 0; i < acc.length; i++) {
      acc[i].addEventListener("click", function() {
        this.classList.toggle("active");
        var panel = this.nextElementSibling;
        if (panel.style.maxHeight) {
          panel.style.maxHeight = null;
        } else {
          panel.style.maxHeight = panel.scrollHeight + "px";
        }
      });
    }

    const charts = document.querySelectorAll('[data-counts]');
      charts.forEach(chart => {
        // Get the data-counts attribute value and split it into an array
        const countsTest = chart.getAttribute('data-counts').split(',');
        const counts = {};

        // Loop over each value in the array and count occurrences
        for (let i = 0; i < countsTest.length; i++) {
          const num = parseInt(countsTest[i]);
          counts[num] = counts[num] ? counts[num] + 1 : 1;
        }

        console.log(countsTest); // Log the countsTest array
        console.log(counts); // Log the counts object

        // Destroy any existing chart instance for the canvas element
        const oldChart = chart.chart;
        if (oldChart) {
          oldChart.destroy();
        }

        // Create a new chart instance for the canvas element
        const myChart = new Chart(chart, {
          type: 'bar',
          options: {
            responsive: true,
            maintainAspectRatio: false,
            indexAxis: 'y',
            scales: {
              x: {
                stacked: true,
                display: false
              },
              y: {
                stacked: true,
                display: false
              }
            },
            plugins: {
              legend: {
                display: false
              }
            }
          },
          data: {
            labels: ["Score"],
            datasets: [{
              data: [counts[2] || 0],
              backgroundColor: "#77ACD8"
            },{
              data: [counts[3] || 0]

            },{
              data: [counts[4] || 0],
              backgroundColor: "#FDC26A"
            },{
              data: [counts[5] || 0],
              backgroundColor: "#FCAE37"
            },{
              data: [counts[6] || 0, counts[7] || 0, counts[8] || 0, counts[9] || 0, counts[10] || 0],
              backgroundColor: "#FCAE37"
            }]
          }
        });
        chart.chart = myChart;
      });

My html

<th:block th:each="round : ${roundCourse.rounds}">
...
<div class="container-fluid">
   <canvas th:data-counts="${round.barChartArray}" th:id="'myChart-' + ${round.roundId}"></canvas>
</div>
</th:block>
...
<script th:inline="javascript">
    let rounds = /*[[${roundsJsonNode}]]*/ {};
</script>

Inside the controller
getCourseByRound(id) just gets a list of CourseByRound by a userId

List<CourseByRound> courseByRounds = getCourseByRound(id);
        List<Round> jsonRounds = new ArrayList<>();
        for (CourseByRound courseByRound : courseByRounds) {
            for (Round round : courseByRound.getRounds()) {
                jsonRounds.add(round);
            }
        }
rounds.sort(Comparator.comparing(Round::getRoundDate).reversed());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
model.addAttribute("roundsJsonNode", jsonRounds);
model.addAttribute("courseByRounds", courseByRounds);

So I can get a barchart in each round now, the problem is the data in the chart is now wrong. It’s always missing by one or has one too many. The console.log shows with this score array:
[2', ' 4', ' 4', ' 2', ' 3', ' 2', ' 3', ' 3', ' 3]
I get this

2:2, 3:4, 4:2, NaN: 1

For whatever reason one of the 2s is put as NaN? What is the issue here?

2

Answers


  1. Chosen as BEST ANSWER

    So for anyone else having the same issue to create a chart in a thymeleaf loop with an accordion. I pass a list of rounds to the javascript, then in the loop pass the roundId. Then create a loop with the roundId's with a chart inside and get the round if using the getRoundId method. Check above for data and more info. Html

    <th:block th:each="round : ${roundCourse.rounds}">
    ...
    <div class="container-fluid">
       <canvas th:roundId="${round.barChartArray}" th:id="'myChart-' + ${round.roundId}"></canvas>
    </div>
    </th:block>
    ...
    <script th:inline="javascript">
        let rounds = /*[[${roundsJsonNode}]]*/ {};
    </script>
    

    javascript

    function getRoundById(rounds, roundId) {
          return rounds.find((round) => round.roundId === Number(roundId));
        }
    
        var acc = document.getElementsByClassName("accordion");
        var i;
    
        for (i = 0; i < acc.length; i++) {
          acc[i].addEventListener("click", function() {
            this.classList.toggle("active");
            var panel = this.nextElementSibling;
            if (panel.style.maxHeight) {
              panel.style.maxHeight = null;
            } else {
              panel.style.maxHeight = panel.scrollHeight + "px";
            }
          });
        }
    
        const charts = document.querySelectorAll('[roundId]');
          charts.forEach(chart => {
            const getRound = chart.getAttribute('roundId').split(',');
            const roundData = getRoundById(rounds, getRound);
    
            const scoreCount = roundData.scores.length;
            const scoreData = {};
    
            for (let i = 0; i < scoreCount; i++) {
              const score = roundData.scores[i];
              if (!scoreData[score.name]) {
                scoreData[score.name] = {
                  count: 0,
                  color: score.color,
                  score: score.score,
                };
              }
              scoreData[score.name].count += 1;
            }
    
            const datasets = [];
            Object.entries(scoreData).forEach(([name, data]) => {
              datasets.push({
                label: name,
                backgroundColor: data.color,
                data: [data.count],
                score: data.score, // add score value to dataset object
              });
            });
    
            datasets.sort((a, b) => a.score - b.score); // sort datasets by score value
    
            const myChart = new Chart(chart, {
              type: 'bar',
              options: {
                responsive: true,
                maintainAspectRatio: false,
                indexAxis: 'y',
                scales: {
                  x: {
                    stacked: true,
                    display: false,
                  },
                  y: {
                    stacked: true,
                    display: false,
                  },
                },
                plugins: {
                  legend: {
                    display: false,
                  },
                },
              },
              data: {
                labels: [''],
                datasets,
              },
            });
          });
    

  2. You can render you entire object rounds as JSON within a thymeleaf inline <script> tag

    <script th:inline="javascript">
      let rounds = /*[[${rounds}]]*/ {};
    </script>
    

    So as per my codepen, the code remains the same

    <html>
    <head>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
    </head>
    <body>
    <div>
      <canvas id="myChart" width="600" height="250"  ></canvas>
    </div>
    <script th:inline="javascript">
      var datasets = [];  
      var label = [];
      var rounds = /*[[${roundsJson}]]*/ {};
      console.log(rounds);
      var i = 0;
      for(var prop in rounds) {
         var dataset = []
         for(var j=0;j<rounds[prop][0].scores.length;j++) {
            dataset.push(rounds[prop][0].scores[j].score)
         }
         console.log(dataset)
         const counts = {};
         for (const num of dataset) {
            counts[num] = counts[num] ? counts[num] + 1 : 1;
         }
         console.log(counts)
         label.push("Score Round " + (i+1))
         i++;
         datasets.push(counts)
      }
      
      console.log(datasets)
      
      var newdatasets = [];
      var keys = Object.keys(datasets[0])
      for(var j=0;j<keys.length;j++) {
         newdatasets.push({
           data: [],
           key: keys[j],
           label: "Score " + keys[j]
         });
      }
      for(var i=0;i<newdatasets.length;i++) {
         for(j=0;j<datasets.length;j++) {
            console.log(datasets[j][newdatasets[i].key]);
            newdatasets[i].data.push(datasets[j][newdatasets[i].key])
         }
      }
      
      console.log(newdatasets)
    
    var ctx = document.getElementById("myChart").getContext("2d");
    var myChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: label,        
            datasets: newdatasets
        },
        options: {
            responsive: true,
            maintainAspectRatio: false,
            indexAxis: 'y',
            scales: {
              x: {
                stacked: true,
                display: false
              },
              y: {
                stacked: true,
                display: false
              }
            },
            plugins: {
              legend: {
                display: false
              }
            },
        }
    });
    </script>
    </body>
    </html>
    

    modify the for loop and change the chart as per your needs

    Regarding your class structure please use this code
    To convert your class structure to plain JSON

    try {
        ObjectMapper mapper = new ObjectMapper();
        Map<Course, List<Round>> mapRoundsByCourse = rounds.stream().collect(Collectors.groupingBy(Round::getCourse));
        JsonNode jsonNode = mapper.valueToTree(mapRoundsByCourse);
        model.addAttribute("roundsJson", jsonNode);
    
    } catch (IOException e) {
              
    

    Then change your <script> to

    <script th:inline="javascript">
      let rounds = /*[[${roundsJson}]]*/ {};
    </script>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search