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?

18
  • 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 jsfiddle.net/code4mk/1j62ey38 Commented Mar 21, 2023 at 14:31
  • please change counts = {}; to counts = []; and use .push() to add elements to it Commented Mar 21, 2023 at 14:34
  • Thank you for getting back so quick, where exactly do I put the .push()? Commented Mar 21, 2023 at 17:09
  • inside the for loop -- for (const num of countsTest) { you put counts.push(counts[num] ? counts[num] + 1 : 1) Commented Mar 21, 2023 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 Commented Mar 21, 2023 at 17:13

2 Answers 2

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/[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> 
Sign up to request clarification or add additional context in comments.

28 Comments

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.
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?
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 ?
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.
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 --- codepen.io/dickensas/pen/JjazdmY
|
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, }, }); }); 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.