Search code examples
javascripthtmlcssreactjscss-selectors

Connecting Lines in a Single Elimination UI with an Odd Number of Players - react


I'm building a Single Elimination UI using React and CSS, and I've got an issue when trying to connect lines between players.

I tried this way. drawn a line over it. but structure got failed. Here's the code i tried https://codesandbox.io/s/single-elimination-torunament-forked-qqlxm4?file=/src/SingleElimination.jsx But it failed due to improper structure of boxes. and lines are not drawing if it not comes under viewport

enter image description here

required structure was

enter image description here

please suggest me with some solutions.


Solution

  • To be honest, i'd prefer to use another HTML for the connecting lines, but I don't want to change your code so much just for that, so in this solution, i'm sticking to use SVG for drawing the lines.

    First thing is that, to make a structure similar to the second picture, you'd need to add the "ghost" box, so I added this:

    const players15 = {
        round1: [
          [], // need to add this
          [8, 9],
    ...
    

    And then I made the CSS so that the box are vertically centered (many CSS codes tweaked, can't really show which ones because there are too many).

    After the CSS is fixed, I realized that you connected some boxes to wrong boxes:

          drawConnectorsBetweenMatches(".round1match2", ".round2match1", newLines);
          drawConnectorsBetweenMatches(".round1match2", ".round2match2", newLines);
          drawConnectorsBetweenMatches(".round1match3", ".round2match1", newLines);
    

    I'd assume that you don't want to connect round1match2 with round2match1, while also connecting round1match2 with round2match2 (like why would one box be connected to two boxes on the next round?), so I fixed them according to the second picture also:

          // round 1 to 2
          drawConnectorsBetweenMatches(".round1match2", ".round2match1", newLines);
          drawConnectorsBetweenMatches(".round1match3", ".round2match2", newLines);
          drawConnectorsBetweenMatches(".round1match4", ".round2match2", newLines);
          drawConnectorsBetweenMatches(".round1match5", ".round2match3", newLines);
          drawConnectorsBetweenMatches(".round1match6", ".round2match3", newLines);
          drawConnectorsBetweenMatches(".round1match7", ".round2match4", newLines);
          drawConnectorsBetweenMatches(".round1match8", ".round2match4", newLines);
    
          // round 2 to 3
          drawConnectorsBetweenMatches(".round2match1", ".round3match1", newLines);
          drawConnectorsBetweenMatches(".round2match2", ".round3match1", newLines);
          drawConnectorsBetweenMatches(".round2match3", ".round3match2", newLines);
          drawConnectorsBetweenMatches(".round2match4", ".round3match2", newLines);
    
          // round 3 to 4
          drawConnectorsBetweenMatches(".round3match1", ".round4match1", newLines);
          drawConnectorsBetweenMatches(".round3match2", ".round4match1", newLines);
    

    After that, I realized that the lines are still not rendered correctly, so I had to tweak the lines placements. Basically I changed your code to this:

        let midPointX =
          (match1Rect.right + match2Rect.left) / 2 + scrollLeft - playoffTableLeft;
    
        newLines.push({
          x1: midPointX,
          y1: match1Rect.top + match1Rect.height / 2 + window.scrollY - offsetTop,
          x2: midPointX,
          y2: match2Rect.top + match2Rect.height / 2 + window.scrollY - offsetTop
        });
        newLines.push({
          x1: match1Rect.right + scrollLeft - 20 - playoffTableLeft,
          y1: match1Rect.top + match1Rect.height / 2 + window.scrollY - offsetTop,
          x2: midPointX,
          y2: match1Rect.top + match1Rect.height / 2 + window.scrollY - offsetTop
        });
        newLines.push({
          x1: match2Rect.left + scrollLeft + 20 - playoffTableLeft,
          y1: match2Rect.top + match2Rect.height / 2 + window.scrollY - offsetTop,
          x2: midPointX,
          y2: match2Rect.top + match2Rect.height / 2 + window.scrollY - offsetTop
        });
    

    Basically, I added them so that the calculations will reflect to viewport changes.

    Then I also removed this line because i'm not sure if it's still necessary:

        playoffTable.addEventListener("scroll", handleScroll);
    

    This is how it looks like now: enter image description here

    Here is the forked sandbox:

    Edit single-elimination-torunament-forked-zxp277

    EDIT:

    Since OP said that they can't manually add the empty boxes, I offer solution where the empty boxes are added automatically, this solution even connects the lines automatically.

    So since it's essentially a binary tree data structure, we could just iterate from the root (the last round) and see if there are any value which aren't "tbd", and if it's not "tbd", we can add empty boxes on the previous round so that the CSS will hold out, this is the function:

      function populateTree(obj) {
        const newObj = JSON.parse(JSON.stringify(obj));
        const round = Math.max(
          ...Object.keys(newObj).map((key) =>
            parseInt(key.replace(/[A-Za-z$-]/g, ""))
          )
        );
    
        let pointer = round;
    
        while (pointer !== 1) {
          const currRound = newObj[`round${pointer}`].flat(Infinity);
    
          for (let i = 0; i < currRound.length; i++) {
            if (currRound[i] !== "tbd") {
              newObj[`round${pointer - 1}`].splice(i, 0, [null, null]);
            }
          }
          pointer--;
        }
    
        return newObj;
      }
    

    This could now handle cases where there are winners already:

    enter image description here

    This is the new forked codesandbox:

    Edit single-elimination-torunament-forked-jj5xty