this is my last try. I've had this issue for several weeks now but with no luck. I have a spring crud app with thymeleaf for a disc golf app. You can add courses and then add rounds for a logged in user (similar to udisc) and it all works great. A user can see all their rounds by course in an accordion. I use a Java Map and loop through Map<Course, List<Round>>
and display it using thymeleaf/html. You click on the course (accordion) then it opens up to all rounds played for that course. Given my limited programming skills Im impressed with what ive done so far.
My problem is (and this is my 3rd try asking this) to have a bar chart in each round inside the accordion. With the code below, I get the desired horizontal bar chart in the first th:each but every round after has nothing. When I refresh the page because the Map is never in the same order the first course in the accordion always has a bar chart for the first round in the loop but never after. So how can I get a bar chart for each round in a Map that is sorted by Map<Course, List<Round>>
?
If Im doing this the wrong way please advise, Im open to all suggestions. I know far less javascript than Java, so treat me as a beginner, but Im assuming there is a fetch() function or something that has to happen again in order for this to work. Asked previously here where there is some screen shots. Thanks in advance.
Course
@Entity
@Table(name = "course")
@Builder
public class Course {
@Id
@Column(name = "course_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "course_id", referencedColumnName = "course_id")
private List<Hole> holes = new ArrayList<>();
@Column(name = "course_par", nullable = false)
private int par;
@Column(name = "record", nullable = false)
private int record;
@Column(name = "course_average", nullable = false)
private double courseAverage;
//getter setters etc
@Entity
@Table(name = "round")
public class Round {
@Id
@Column(name = "round_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long roundId;
@JsonIgnore
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "round_id", referencedColumnName = "round_id")
private List<Score> scores = new ArrayList<>();
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@Column(name = "round_date")
@DateTimeFormat(pattern = "dd/MM/yyyy")
private Date roundDate;
@Column(name = "round_total")
private int total;
//getter setters etc
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();
rounds.sort(Comparator.comparing(Round::getRoundDate).reversed());
Map<Course, List<Round>> mapRoundsByCourse = rounds.stream().collect(Collectors.groupingBy(Round::getCourse));
model.addAttribute("userId", id);
model.addAttribute("roundService", roundService);
model.addAttribute("courses", courses);
model.addAttribute("rounds", mapRoundsByCourse);
return "/discgolf/round/rounds";
}
html
<th:block th:each="round : ${roundCourse.value}">
<div id="cardBody" 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 id="myChart"></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:text="${score.score}" th:style="'background-color: ' + ${score.color}">
</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>
</th:block>
Javascript
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"
}]
}
}
);
Example 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}]
roundService
public List<Integer> getListOfScoresByRoundId(Long id) {
return scoreRepository.findAllScoresByRoundId(id);
}
scoreRepository
@Repository
public interface ScoreRepository extends CrudRepository<Score, Long> {
@Query(value = "SELECT s.score from score s WHERE s.round_id = :id", nativeQuery = true)
List<Integer> findAllScoresByRoundId(@Param("id") Long id);
}