0

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?

  • there must be something with your code clarity and clean coding, you can start with this code and try to modify it with plain html https://jsfiddle.net/code4mk/1j62ey38/ – Dickens A S Mar 21 '23 at 14:31
  • please change `counts = {};` to `counts = [];` and use `.push()` to add elements to it – Dickens A S Mar 21 '23 at 14:34
  • Thank you for getting back so quick, where exactly do I put the .push()? – Kayd Anderson Mar 21 '23 at 17:09
  • inside the for loop -- `for (const num of countsTest) {` you put `counts.push(counts[num] ? counts[num] + 1 : 1)` – Dickens A S Mar 21 '23 at 17:11
  • also try to make you entire code a pure html and work on it first before you do your thymeleaf code, and refer the jfiddle URL I gave above – Dickens A S Mar 21 '23 at 17:13
  • sorry but that didn't work, still the same – Kayd Anderson Mar 21 '23 at 17:16
  • so do you mean its bad to have a method call inside my html? Is it better to build the array needed for the chart in js? If so how do I do that? If I understand it correctly the array that the bar chart needs should be constructed in js and not called as a method in HTML? – Kayd Anderson Mar 21 '23 at 18:18
  • the basic ideology of thymeleaf or any other java/kotlin/groovy backed template web server code, once it reaches browser, it will be pure HTML and js – Dickens A S Mar 24 '23 at 12:50
  • you can view the code using right click view source on the browser or right click inspect and get the code out and complete the javascript portion of it – Dickens A S Mar 24 '23 at 12:51
  • Thanks for that, I do use the inspect option in my browser to see if the data is there/correct. But thats not my question, again, can someone show me how to have the bar graph in each round displayed? – Kayd Anderson Mar 25 '23 at 19:47
  • each `round` depends on `${roundCourse.value}` which is rendered on the UI as table, please give me `${roundCourse.value}` as JSON we will discuss which part that json needs to be converted to bar chart, then I will give you proper code for `chartjs` – Dickens A S Mar 27 '23 at 07:36
  • Thanks again, ive add the data and how I construct the data in the controller. My apologies if I misunderstood. – Kayd Anderson Mar 27 '23 at 08:20
  • I have created a code for the JSON structure for `total` for each round -- https://codepen.io/dickensas/pen/JjazdmY, please let me know which portion of the nested data you want bar chart, you can see the JSON data inside the javascript editor – Dickens A S Mar 27 '23 at 13:45
  • Thanks again for your time, the codepen you showed me is a simple bar chart which is fine and I can do. The link I posted above it shows more details of what I originally asked and an update. Here: https://stackoverflow.com/questions/75744414/best-practice-for-creating-a-dynamic-horizontal-bar-chart-using-spring-thymelea – Kayd Anderson Mar 28 '23 at 14:22
  • I can get the data its just showing the horizontal bar chart in each loop for each th:each="round : ${roundCourse.value}"> – Kayd Anderson Mar 28 '23 at 14:28
  • no you can generate a ` – Dickens A S Mar 28 '23 at 14:58
  • Ok, I've used something like that such as at the bottom of my html the var listRounds = [[${rounds}]]. But never with [#th:block th:each. Do you mind showing an example? – Kayd Anderson Mar 28 '23 at 18:21
  • given the code on the answer, you can directly render your entire model attribute as JSON, on top of it you can loop at level you want, `total` `holes` `scores` anything you can plot in chartjs – Dickens A S Mar 29 '23 at 10:05

2 Answers2

1

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/chart.js@4.2.1/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>
Dickens A S
  • 3,824
  • 2
  • 22
  • 45
  • Thanks again, I do understand that part, but my problem I don't want the total. In the html Im calling th:attr="data-counts=${roundService.getListOfScoresByRoundId(round.roundId)}", which (if I understand you correctly) is bad practice. This method returns a list of scores by that round's id. Which then I use ```for (const num of countsTest)``` to get how many times the round has that score. Eg if the score is 3, 3, 3, 4, 4, 2, 4, 2 I don't know exactly how it looks but its 3 - 3s, 3 - 4s, 2 - 2's. Thats the single horizontal bar chart. – Kayd Anderson Mar 29 '23 at 10:44
  • So how do I recreate my getListOfScoresByRoundId in javascript looping your rounds = /*[[${rounds}]]*/ {};? Can you use my graph too as its hard for me to follow? – Kayd Anderson Mar 29 '23 at 10:45
  • I asked the same question before, this is the first time you told that you actually want `score` you want to incorporate in chartjs, so how do want the bar chart ? stacked bar ? invidual bar ? which one you want ? – Dickens A S Mar 29 '23 at 11:20
  • My apologies, but again its all in the link to the previous question. I've added a screen shot of what I want. The problem is the horizontal bar is only displayed on the first for each. I want one for each round. Thanks again. – Kayd Anderson Mar 29 '23 at 15:18
  • done the final code with double for loop, you can use and enhance it, codepen also modified, you can fork codepen and play with it --- https://codepen.io/dickensas/pen/JjazdmY – Dickens A S Mar 30 '23 at 06:38
  • Thanks again, but I got an error, I've added an update above. Is the data correct? I may be wrong but I have used a Map>., have you used a list of rounds? – Kayd Anderson Mar 30 '23 at 10:58
  • your console.log is printing a non JSON bean to string, you need to do a small java code to convert you class object to JSON using Jackson faster xml ObjectMapper – Dickens A S Mar 31 '23 at 08:38
  • I have added the code which converts `mapRoundsByCourse` to json and put the output inside `roundsJson` which can be used for javascript coding – Dickens A S Mar 31 '23 at 09:34
  • Really, thanks again for your efforts but now rounds is null, I've edited my update. – Kayd Anderson Mar 31 '23 at 09:53
  • check exception `IOException e` put `e.printStackTrace()` – Dickens A S Mar 31 '23 at 10:18
  • Yeah I did and I was trying to figure out this error with an InvalidDefinitionException : Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", I've been trying to fix it with this: https://stackoverflow.com/questions/74188846/how-to-fix-jackson-java-8-data-time-error but Im not having any luck. When I do add one of the UPDATE examples from the link the error goes away but the bar chart still doesnt show. – Kayd Anderson Mar 31 '23 at 18:29
  • Could only do UPDATE 2 examples from the link, could workout how to do the others with the JacksonConfiguration class. – Kayd Anderson Mar 31 '23 at 18:35
  • Im still having no luck with this. – Kayd Anderson Apr 04 '23 at 15:02
  • please share the model classes I will form the json from it, you can share via pastebin or any other website – Dickens A S Apr 11 '23 at 07:39
  • Thanks again for helping out. I added the models below my last edit. – Kayd Anderson Apr 12 '23 at 14:39
  • I have a doubt `rounds.stream().collect(Collectors.groupingBy(Round::getCourse));` is this a grouping by course? is it not possible to map courses by round ? I thought for each round we need a chart not for each course – Dickens A S Apr 13 '23 at 10:00
  • changed the javascript for loop according to the map `Map>` --- the for loop is `for(var prop in rounds)` and inside the loop I am using inner loop --- `for(var j=0;j – Dickens A S Apr 13 '23 at 10:19
  • Dude, thank you again for your time, but I'm getting an error: Uncaught TypeError: Cannot convert undefined or null to object at Function.keys () at userRounds.js:34:25 row 34 is ```var keys = Object.keys(datasets[0])``` Honestly if there is a better way to do this I'm open to it. I tried using a Map because I wanted the accordion to be sorted by course, then with all rounds for that course inside. Then I tried the bar graph and got stuck here. Again thanks heaps. – Kayd Anderson Apr 13 '23 at 19:21
  • for a json in java code similar to my json in -- codepen.io/dickensas/pen/JjazdmY -- that will solve your problem – Dickens A S Apr 14 '23 at 05:52
  • Ok, progress for me. So the jSon works in your codepen example but only if I physically copy a round part into your codepen. I added a screen shot, again I'm a beginner with this, but your javascript looks like it only loops the rounds but the json data is a course with an array of rounds for each course. As you can see the highlighted part in the screen shot is a round. – Kayd Anderson Apr 14 '23 at 08:22
  • that is confusing, I though for each round you want 1 bar, but now I am getting news that for each course you want 1 bar stacked with scores ? then your model should be programmed like that, there is course inside round class inside the course class that score array is there – Dickens A S Apr 14 '23 at 08:25
  • Yeah I know it is, (excuse my ignorance) but a course in the model doesnt have a relationship to a round, but I managed to make a Map> mapRoundsByCourse that works good with all the data displayed in the accordions, then this begins the root of my problem. An accordion's label name is a course, then it opens up to the different rounds for that course. The for each round (along with all the other data) there is a single horizontal bar. Would looping each course and getting the rounds work? – Kayd Anderson Apr 14 '23 at 08:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253149/discussion-between-dickens-a-s-and-kayd-anderson). – Dickens A S Apr 14 '23 at 09:10
  • yes, `dataset.push(rounds[prop][0].scores[j].score)` should do that job, put console.log everywhere and see what happens, otherwise the score array list empty then my code won't work – Dickens A S Apr 14 '23 at 09:11
  • Sorry, I missed your chat. I've added an update, changed things around. Hopefully Im closer. Again thanks heaps for your efforts. – Kayd Anderson Apr 14 '23 at 18:55
  • I see your bar chart is already showing score types like 1s, 2s, 3s, 4s, staked on each other per round, I think we can call it as a success – Dickens A S Apr 15 '23 at 16:37
  • Well the problem now is all bar charts are in one round. Original question, how do I have one bar chart for each round? Each score has a color and name too, so it would be good to set the bar colors too. – Kayd Anderson Apr 16 '23 at 11:43
  • Made some some progress but still not right, please see update – Kayd Anderson Apr 17 '23 at 10:53
0

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