0

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);
}
  • You are asking us to debug a fairly large set of code, with only parts of the code shown in the question, and not much data. This may boil down to something quite simple - but that is somewhat lost in the details. Can I suggest an alternative? (1) Give us a ruthlessly pared down [mre]. (2) The only server-side code we need is something which provides hard-coded test data for the Thymeleaf template to iterate over. No DB code, no entity annotations... – andrewJames Apr 07 '23 at 19:58
  • (3) The Thymeleaf template can probably be reduced to a fraction of its current code - just the code for `th:each="roundCourse : ${rounds}"...` for the accordion and that part of the ` – andrewJames Apr 07 '23 at 19:59
  • Having said that... you have `` - and that `myChart` ID will be generated multiple times in the final HTML page - because it's in a loop. And now you have duplicate HTML IDs - bad! They are supposed to be unique. And then you use `document.getElementById('myChart')` - which finds "the" one element with that "unique" ID... so it only finds [one element](https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById). Probably the first one it finds in the doc - and then maybe it stops looking. – andrewJames Apr 07 '23 at 19:59
  • 1
    Ah ok, I'll put that down to me not knowing javascript. Ive reduced the html and added the data of one course with two rounds. Anything else? The last time I asked this question I was helped with this codepen example https://codepen.io/dickensas/pen/JjazdmY but I couldnt get it to work. – Kayd Anderson Apr 07 '23 at 20:23
  • Thank you for the updates. I can see you have put a large amount of work into all of this. But I cannot run your code. It is not a MRE. How do I use that sample data? It is not runnable. What is the data for `roundService.getListOfScoresByRoundId(round.roundId)`? And so on. If you yourself cannot run the code as a MRE (without a lot of edits) it's unlikely we will be able to. It's possible someone will be able to help you without all of this - but I can't - I'm not smart enough. – andrewJames Apr 07 '23 at 20:40
  • I've added the getListOfScoresbyRoundId(). Thank you for trying, I honestly feel this shouldn't be too complicated. Maybe Im making it complicated? I'd be happy to see an example of a chart rendering in a thymeleaf loop. – Kayd Anderson Apr 07 '23 at 20:54
  • Suggestion - start here: (1) In your `` tag change `id="myChart"` to `class="myChart"`. (2) In your JavaScript code, change that initial selector from `getElementById('myChart')` to `getElementsByClassName('myChart')` - note the plural "elements" here. This will select _all_ your `` elements - not just one. Now, your JavaScript code will have a collection of elements it can process in your JavaScript - similar to how you process a set of `` elements. – andrewJames Apr 08 '23 at 13:30
  • (3) You need to loop through each `` element and create each chart inside that loop, using the relevant data. This means you also need to fix `new Chart(document.getElementById('myChart')`. The above suggestions are just a start, to see if that at least gets multiple charts created (maybe it won't?). Also, ideally, in your Thymeleaf, you should be populating each separate ` – andrewJames Apr 08 '23 at 13:31
  • Thanks again for trying, do you mind writing an answer? I get changing the 2 getElementById to getElementsByClassName. But the loop in step 3 I'm having not exactly sure how to do it correctly. – Kayd Anderson Apr 09 '23 at 10:04

1 Answers1

0

Here is a minimal reproducible example which focuses on one specific problem: displaying multiple charts in one page, where each chart uses its relevant data.

Some assumptions:

  1. There is no Thymeleaf or Java code, because the objective, as stated above, is only to draw multiple charts. The chart code runs after all Thymeleaf and Java has finished - and the page only contains HTML and JavaScript. So, all of that prior processing is not relevant here.

  2. I have assumed that the Thymeleaf generates HTML which contains only 2 charts. Obviously your Thymeleaf loop will generate more chart HTML. But again this is a MRE - it contains nothing which is not relevant to the specific problem.

  3. I do not know what the data looks like for data-counts=${roundService.getListOfScoresByRoundId(round.roundId)} - that is missing from the question (or I missed it, if it is in there somewhere). Therefore I have provided my own data: data-counts="[[1,2,3],[4,5,6]]". This is almost certainly not the same as your data. So you may need to take that into account... and adjust my approach for that.

  4. My JavaScript code loops through every chart in the page, and extracts the relevant data array from [[1,2,3],[4,5,6]]. I have 2 charts - so there are 2 data arrays: [1,2,3] for the first chart and [4,5,6] for the second chart. Yes, this is all hard-coded data (as per my MRE). It assumes your Thymeleaf and Java code provides this data correctly.

  5. You can uncomment the JavaScript console.log() statements to see what the code is doing, as it loops through each chart.

Enough already. Here it is - just click the blue run button:

const charts = document.getElementsByClassName('myChart');

for (let i = 0; i < charts.length; i++) {

  const countsTest = charts[i].getAttribute('data-counts');
  //console.log( charts[i] );
  //console.log( JSON.parse( countsTest ) );
  counts = JSON.parse(countsTest)[i];
  //console.log( counts );

  new Chart(charts[i], {
    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[0]],
        backgroundColor: "#77ACD8"
      }, {
        data: [counts[1]]
      }, {
        data: [counts[2]],
        backgroundColor: "#FDD79C"
      }]
    }
  });

}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <title>Rounds</title>
</head>

<body>

  <div>
    <canvas data-counts="[[1,2,3],[4,5,6]]" class="myChart"></canvas>
  </div>

  <div>
    <canvas data-counts="[[1,2,3],[4,5,6]]" class="myChart"></canvas>
  </div>

</body>

</html>

You can see that this approach contains the same chart data, repeated multiple times in the HTML: [[1,2,3],[4,5,6]].

In other words, you are provide all the chart data for all charts in each chart tag.

Really, you should refactor that to only provide the relevant chart data in each chart:

<div>
    <canvas data-counts="[1,2,3]" class="myChart"></canvas>
</div>

<div>
    <canvas data-counts="[4,5,6]" class="myChart"></canvas>
</div>

You may want to look at making this change after you get what you have to work.


My apologies if this is not what you need, or is not helpful.

But at least it may give you some pointers - and it shows what I mean by a MRE. A good MRE is focused, limited in scope, and is easy for us to copy/paste and run for ourselves.

andrewJames
  • 19,570
  • 8
  • 19
  • 51