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?
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>