GSAP + SplitType + i18n text fails to update correctly after language change

I’m using GSAP with SplitType and a custom language switcher (en.json, es.json) to translate my website dynamically.
Everything translates correctly except the .scroll-fade-text section — which uses SplitType for scroll-triggered word-by-word animation.

On language change, the translated text briefly appears, then reverts back to the original language’s content (probably due to SplitType or GSAP misfiring).

What I’ve Tried:

Verified the translation keys load and update correctly via data-i18n

Used requestAnimationFrame() and setTimeout() to delay SplitType initialization

Used .revert() and .kill() to clean up previous animation

HTML

<div class="scroll-lock-section">
  <div class="scroll-text-wrap scroll-fade-text" data-i18n="scrollLine">
    MY LONG TEXT IN ENGLISH HERE.
  </div>
</div>

JS

document.addEventListener('DOMContentLoaded', () => {
  const langSelect = document.getElementById('langSelect');
  const storedLang = localStorage.getItem('lang') || 'en';
  langSelect.value = storedLang;

  applyLanguage(storedLang);

  langSelect.addEventListener('change', () => {
    const lang = langSelect.value;
    localStorage.setItem('lang', lang);
    applyLanguage(lang);
  });
});

function applyLanguage(lang) {
  fetch(`/lang/${lang}.json`)
    .then(res => res.json())
    .then(data => {
      document.querySelectorAll('[data-i18n]').forEach(el => {
        const key = el.getAttribute('data-i18n');
        const allowHtml = el.getAttribute('data-i18n-html') === 'true';
        if (data[key]) {
          if (allowHtml) {
            el.innerHTML = data[key];
          } else {
            el.textContent = data[key];
          }
        }
      });

      // Attempt delayed animation init
      requestAnimationFrame(() => {
        setTimeout(() => {
          initScrollAnimation();
        }, 100);
      });
    });
}

let scrollSplit;
function initScrollAnimation() {
  if (scrollSplit) scrollSplit.revert();

  ScrollTrigger.getAll().forEach(trigger => trigger.kill());
  gsap.killTweensOf("*");

  scrollSplit = new SplitType(".scroll-fade-text", {
    types: "words",
    wordClass: "word"
  });

  gsap.to(scrollSplit.words, {
    color: "white",
    stagger: 0.3,
    scrollTrigger: {
      trigger: ".scroll-lock-section",
      start: "top top",
      end: "+=1000",
      scrub: true,
      pin: true,
      invalidateOnRefresh: true
    }
  });
}

CSS

.scroll-lock-section {
  height: 85vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 20px;
  background-color: #0c1624;
}

.scroll-text-wrap {
  font-size: 48px;
  font-family: 'Manrope', sans-serif;
  font-weight: bold;
  max-width: 1000px;
  text-align: center;
  line-height: 1.5;
  margin-bottom: -150px;
}

.scroll-text-wrap .word {
  color: gray;
  transition: color 0.4s ease;
}

When I switch the language from English to Spanish:

All other data-i18n elements update correctly.

scroll-fade-text briefly changes, then reverts back to English.

The GSAP animation continues working, but with the old content.

How can I properly re-split and animate translated content inside .scroll-fade-text using SplitType + GSAP without it resetting or reverting?

All translation is client-side using data-i18n and a JSON map.

Working example…

HTML

<a class="contact-button" href="mycontact" target="_blank" rel="noopener" data-i18n="navContact">Contact us</a>

JSON

en.json

  "navContact": "Contact us",
/* more below */

es.json

  "navContact": "Contáctanos",
/* more below */

Why Overriding MIME Type works but not Response Type?

I am new to ‘PHP and JavaScript’. The other day, I was coding for asynchronous request and response. Working sometime with fetch(), I thought of using the XMLHttpRequest(). It was a bit of work, but then I entered the following code:

<script>
let xhr = new XMLHttpRequest();

xhr.open('POST', 'http://localhost/test/test.php', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
xhr.responseType = 'json';

xhr.onload = function() {
    if (xhr.status != 200){
        console.log(`Error ${xhr.status}: ${xhr.statusText}`);
    } else {
        document.getElementById("lull").innerHTML = xhr.response
    }
};

xhr.send("url=wikipedia.org");
</script>

I tried whole night debugging why this is returning null in its response. Then I stumbled upon this and casually changed the xhr.responseType = 'json'; to xhr.overrideMimeType("application/json"); and it worked! So, I am trying to understand how the two differs in their working? The mdn_website says responseType() can be used to change the response type, and many other sources have used it!

The php file that open is refrencing is just listening for url key in the $_POST array and is responding with the html web page using file_get_contents() and encoding it using json_encode().

My browser dev tools -> Application -> Cookies displays the httpOnly cookies set by my backend [duplicate]

my nodejs backend sets this cookie:

res.cookie("refresh_token", refresh_token, {
    httpOnly: true,
    secure: true, // must be true when sameSite: "None",
    sameSite: "None",
    maxAge: 1000000, // maxAge is in milliseconds
  });

Despite this, I can still open of Chrome Dev Tools –> Application –> Cookies to view this cookie plainly.

However, I can confirm that it cannot be access via javascript:

document.cookie

Is this how it is supposed to be? Or is the httpOnly cookie supposed to not even be accessible manually as well?

Important advice for a beginner in programming [closed]

I have started learning programming now. I want to continue in this field, but I do not know if it is a good idea. Is it good to start learning now with the presence of all these new tools such as artificial intelligence? What is your advice to me? I am a refugee trying to learn to get a job. How do I start? I hope to benefit from your experiences. Thank you.

I’m in the learning phase now but I still feel lost.

What does this reduce() function do in counting elements from an array?

I’m trying to understand how the reduce() function works in this example:

const fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];

const count = fruits.reduce((tally, fruit) => {
    tally[fruit] = (tally[fruit] || 0) + 1;
    return tally;
}, {});

console.log(count);

What does (tally[fruit] || 0) mean?

I understand how the flow works but i don’t get this part:
tally[fruit] = (tally[fruit] || 0) + 1;

How do I display JSON data in HTML?

This is my first time using an API. I have the following JavaSript code that I found from a tutorial but I’m not sure how to display it in HTML. I’m trying to access the Weatherbit Current Weather API.

<script>

//WeatherBit API URL and Key
    const apiKey = 'gonna leave this out';
    const apiUrl = `https://api.weatherbit.io/v2.0/current?lat=42.4226814&lon=-94.8580528&key=${apiKey}&include=alerts`;

    const requestOptions = {
        method: 'GET',
            },
        };

    //For HTML Output
    const outputElement = document.getElementById('output');

    //GET request
    fetch(apiUrl)
    .then(response => {
        if (!response.ok) {
        if (response.status === 404) {
            throw new Error('Data not found (404)');
        } else if (response.status === 500) {
            throw new Error('Server Error (500)')
        } else {
            throw new Error('Network response was not ok');
            }
        })
    return response.json()
    
    
    };
        })
    .then(data => {
        //display in HTML
        outputElement.textContent = JSON.stringify(data, null, 2);
        })
    .catch(error => {
        console.error('Error:', error);
        });

</script>

and in the HTML I have this:

 <p id="output">Loading Weather Data...</p>

CSS – Divs are shifting vertically due to SVGs inside

I have a project I am working on that takes data from a database of a card game I am creating and displays it to a user. I am struggling with displaying a list of cards to the user, because some of the div items are not aligned, despite them having the same size and only differ by content.

I have been able to deduce so far that the issue is originating with an embedded SVG in a div alongside text, as every card that is misaligned has an SVG in the card’s title. But I cannot figure out why. It seems that if I make the SVG smaller, the problem goes away, but if it is too small, the card instead moves down. I have tried numerous display types to no avail. The SVG is much smaller than the text (I’d honestly want it bigger as well), and yet it seems to control where the card is positioned vertically.

I think that most likely it is due to the mess of CSS rules I have to display an individual card, and something is conflicting. Or maybe there’s something I’m missing? I realize ther

Apologies for the messy code. I’m not sure what’s to be expected or needed as far as code here, so I’ve pretty much just pasted the whole WIP file here. Not included is the card data that is actually passed, but that is simple JSON data containing strings and integers that are interpreted by the javascript.

image of what the page looks like

cardSearch.html: Displayed to the user, takes cardResults (array containing cards) and repeatedly adds card.html to the page with each card’s data.

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8" />
        <title>CE War - Card Search</title>
        <link rel="stylesheet" type="text/css" th:href="@{style.css}">
        <link rel="icon" type="image/png" th:href="@{images/logo.png}">
        <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    </head>
    <body>
        <div id="card-results">
            <div th:each="aCard : ${cardResults}" class="card-display" style="--card-height: 400px">
                <div th:replace="~{fragments/card :: card(${aCard})}"></div>
            </div>
        </div>
    </body>
</html>

card.html (fragment that is repeated in cardSearch.html)

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8" />
        <title>CE War - Card Display</title>
        <link rel="stylesheet" type="text/css" th:href="@{style.css}">
        <link rel="icon" type="image/png" th:href="@{images/logo.png}">
        <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    </head>
    <body>
        <!-- Insert the container located at fragments/header with the tag "th:fragment="header"" -->
        <div th:insert="~{fragments/header :: header}"/>

        <main>

            <div class="card-table blank-card" th:fragment="card(cardData)">
                <div class="header dark-color">
                    <div class="name"></div>
                    <img class="rarity">
                </div>
                <img class="art">
                <p class="desc light-color"/>
                <div class="footer dark-color">
                    <div class="attack"></div>
                    <div class="center">
                        CE 2nd
                    </div>
                    <div class="health"></div>
                </div>
                <script th:inline="javascript">
                    // Needed for Thymeleaf to be able to run this code
                    /*<![CDATA[*/

                    createCard(/*[[${cardData}]]*/[0]);

                    function createCard(card) {

                        // SECTION Script Functions

                        /** Constructs a card symbol in the form of an html span element.
                         * This element contains a svg with size 1em of the given symbolType, and the same color as the card's type.
                         * This element has the "card-symbol" class.
                         * 
                         * @param symbolType - which symbol to create. Valid symbols are c1, c2, c3, c4, stage, equip, god, back-slot, [] 
                         */
                        function formatSymbol(symbolType) {
                            // Determine which symbol to use
                            switch (symbolType.toLowerCase()) {
                                case "c1": case "c2": case "c3": case "c4": 
                                case "stage": case "equip": case "god": 
                                    // symbol type is valid
                                    var symbolID = symbolType.toLowerCase();
                                    break;
                                case "back-slot": case "[]":
                                    // symbol type is valid
                                    var symbolID = "back-slot";
                                    break;
                                default:
                                    var symbolID = "";
                                    // Log error to website console. Symbol should end up not appearing.
                                    console.error("Invalid symbol type of type "" + symbolType + """);
                            }

                            // Valid types: CREATURE, ACTION, BUILDING, MATERIAL, GOD, REPLICA, OTHER
                            switch (card.type) {
                                case "CREATURE": case "ACTION": case "BUILDING": case "MATERIAL": case "GOD": case "REPLICA":
                                    var symbolColor = `var(--${card.type.toLowerCase()}-color)`;
                                    break;
                                case "OTHER":
                                    if (card.id == "gap") {
                                        var symbolColor = "var(--gap-color)";
                                        break;
                                    }
                                    // else: Continue to default case
                                default:
                                    var symbolColor = "#000000";
                                    // Log error to website console. Symbol color should be black.
                                    console.error("Invalid card type of type "" + card.type + """);
                            }

                            // Combine strings into final html element
                            return `<svg class="card-symbol">
                                            <use href="images/card_symbol.svg#${symbolID}" style="--icon-color: ${symbolColor};"/>
                                        </svg>`;
                        }

                        // Formats card effects to include line breaks, italics, and lists.
                        function formatEffect(string) {
                            const listRegex = new RegExp("(^[0-9]+: )|(^- )");

                            // Split effect, based on line breaks for further processing
                            let cardEffectArr = string.split("n");

                            for (let i = 0; i < cardEffectArr.length; i++) {
                                if (listRegex.test(cardEffectArr[i])) {
                                    // If this is a list item

                                    if (i == 0 | cardEffectArr[i-1].charAt(0) != "<") {
                                        // If previous item was not a list item
                                        if (cardEffectArr[i].charAt(0) == "-") {
                                            var isOrdered = false;
                                            cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
                                            cardEffectArr[i] = "<ul>" + cardEffectArr[i];
                                        } else {
                                            var isOrdered = true;
                                            cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
                                            cardEffectArr[i] = "<ol>" + cardEffectArr[i];
                                        }
                                    } else {
                                        // Ensure this is a list item
                                        // This allows the regex to work properly and avoid issues
                                        cardEffectArr[i] = cardEffectArr[i].replace(listRegex, "<li>") + "</li>";
                                    }

                                    if (cardEffectArr[i].charAt(0) == "<" && i == cardEffectArr.length - 1) {
                                        // If this is the last item in the list
                                        if (isOrdered) {
                                            cardEffectArr[i] = cardEffectArr[i] + "</ol>";
                                        } else {
                                            cardEffectArr[i] = cardEffectArr[i] + "</ul>";
                                        }
                                    }
                                } else {
                                    // Not a list item
                                    if (i > 0 && listRegex.test(cardEffectArr[i - 1])) {
                                        // If previous index was a list item
                                        if (isOrdered) {
                                            cardEffectArr[i - 1] = cardEffectArr[i - 1] + "</ol>";
                                        } else {
                                            cardEffectArr[i - 1] = cardEffectArr[i - 1] + "</ul>";
                                        }
                                    }
                                    cardEffectArr[i] = cardEffectArr[i] + "<br>";
                                }
                            }

                            let cardEffect = cardEffectArr.join("");
                            const symbolRegex = new RegExp("\(\S+\)", "g");
                            cardEffect = cardEffect.replaceAll(symbolRegex, function(match) {
                                return formatSymbol(match.slice(1,-1)); // Remove outer parentheses
                            });

                            return cardEffect;
                        }

                        // Constructs card description field from multiple data sources.
                        function constructCardDesc() {
                            let cardArchetypes = "";
                            if (card.archetypes.length > 0) {
                                // Format archetypes to be lowercase, since the enums are declared as uppercase
                                for (let i = 0; i < card.archetypes.length; i++) {
                                    card.archetypes[i] = card.archetypes[i].charAt(0) + card.archetypes[i].slice(1).toLowerCase();
                                }
                                cardArchetypes = "<em>" + card.archetypes.join(", ") + "</em><br>";
                            }
                            let cardMaterials = "";
                            if (card.materials.length > 0) {
                                cardMaterials = "<em>" + card.materials + "</em><br>";
                            }
                            let cardFlavorText = "";
                            if (card.flavorText.length > 0) {
                                if (card.effect.length > 0) {
                                    cardFlavorText = "<br>";
                                }
                                cardFlavorText = cardFlavorText + "<em>" + card.flavorText + "</em>";
                            }
                            
                            let cardEffect = formatEffect(card.effect);

                            return cardArchetypes + cardMaterials + cardEffect + cardFlavorText;
                        }
                        // !SECTION

                        console.log(card);

                        // Pick the first blank card
                        var cardDiv = $(".card-table.blank-card").first();
                        // Prevent this card from being selected again
                        cardDiv.removeClass("blank-card");

                        // Construct card name field, adding any needed attribute symbols
                        let cardAttributes = [];
                        for (let i = 0; i < card.attributes.length; i++) {
                            cardAttributes[i] = formatSymbol(card.attributes[i]);
                        }
                        cardDiv.find(".name").html(cardAttributes.join("") + " " + card.name);

                        // Determine card rarity
                        switch (card.rarity) {
                            case "COMMON":
                                cardDiv.find(".rarity").attr("src", "images/rarity_common.png");
                                break;
                            case "UNCOMMON":
                                cardDiv.find(".rarity").attr("src", "images/rarity_uncommon.png");
                                break;
                            case "RARE":
                                cardDiv.find(".rarity").attr("src", "images/rarity_rare.png");
                                break;
                            case "ULTRA_RARE":
                                cardDiv.find(".rarity").attr("src", "images/rarity_ultra_rare.png");
                                break;
                            case "NONE":
                                if (card.type == "GOD") {
                                    // Stretch card text div to take up entire width of card.
                                    cardDiv.find(".name").css("width", "calc(125% - 10px)"); // Accomodate border space
                                    cardDiv.find(".rarity").style = "display: none;" // Hide rarity symbol
                                }
                                break;
                            default:
                        }

                        // Add Card Art
                        cardDiv.find(".art").attr("src", card.artSource);

                        // Construct card description
                        cardDiv.find(".desc").html(constructCardDesc());

                        // Determine left footer text
                        if (card.attack == -1) {
                            if (card.size.length == 2) {
                                cardDiv.find(".attack").text(card.size[0] + "x" + card.size[1]);
                            } else {
                                cardDiv.find(".attack").text("");
                            }
                        } else if (card.attack == -2) {
                            cardDiv.find(".attack").text("???");
                        } else {
                            cardDiv.find(".attack").text(card.attack);
                        }

                        // Determine right footer text
                        if (card.health == -2) {
                            cardDiv.find(".health").text("???");
                        } else {
                            if (card.health == -1) {
                                cardDiv.find(".health").text("");
                            } else {
                                cardDiv.find(".health").text(card.health);
                            }
                            if (card.god) {
                                cardDiv.find(".health").append(formatSymbol("back-slot")); // Could also do "[]" instead
                            }
                        }

                        // Determine card color
                        switch(card.type) {
                            case "CREATURE":
                                var darkColor = "#7fdc39";
                                var lightColor = "#b6d7a8";
                                break;
                            case "ACTION":
                                var darkColor = "#4c4cff";
                                var lightColor = "#9fc5e8";
                                break;
                            case "BUILDING":
                                var darkColor = "#ff0000";
                                var lightColor = "#ea9999";
                                break;
                            case "MATERIAL":
                                var darkColor = "#ffff00";
                                var lightColor = "#ffe599";
                                break;
                            case "GOD":
                                var darkColor = "#ff9900";
                                var lightColor = "#f9cb9c";
                                break;
                            case "REPLICA":
                                var darkColor = "#b7b7b7";
                                var lightColor = "#d9d9d9";
                                break;
                            default:
                                var darkColor = "#ffffff";
                                var lightColor = "#ffffff";
                        }
                        cardDiv.find(".dark-color").css("background-color", darkColor);
                        cardDiv.find(".light-color").css("background-color", lightColor);
                    }

                    // Adjust text scaling (at least trying to)
                    // TODO

                    /*]]>*/
                </script>
            </div>
        </main>

    </body>
</html>

style.css (properties relating to other pages have been omitted)

:root {
    /* Colors used for cards. Maybe should be moved elsewhere. */
    --creature-color: #7fdc39;
    --action-color: #4c4cff;
    --building-color: #ff0000;
    --material-color: #ffff00;
    --god-color: #ff9900;
    --replica-color: #b7b7b7;
    --gap-color: #c934e8;
}

/* used for cards */
@property --icon-color {
    syntax: "<color>";
    inherit: false;
    initial-value: var(--action-color);
}

/* variable way to modify card height */
@property --card-height {
    syntax: "<length>";
    inherit: true;
    initial-value: 600px;
}

main {
    padding: 10px;
    font-family: Arial;
}

body {
    padding: 0px;
    margin: 0px;
}

header {
    text-align: center;
    background-color: #3C0;
    padding: 50px;
    box-sizing: border-box;
}

/* Navigation bar on all pages */
nav {
    text-align: center;
    background-color: #aaa;
    padding: 10px;
    box-sizing: border-box;
}

/* Anchor tags inside navigation bar */
nav a {
    display: inline-block;
    border: 1px solid black;
    padding: 5px;
    box-sizing: border-box;
}

.card-table {
    display: grid;
    grid-template-areas:
        "header"
        "art"
        "desc"
        "footer";
    grid-template-rows: 12% 34% 42% 12%;
    grid-template-columns: 100%;
    width: calc(var(--card-height) * 0.725);
    height: var(--card-height);
    border: 5px solid black;
    border-radius: 5px;

    div {
        display: grid;
        margin: 0px 0px 0px 0px;
        padding: 5px;
        border-radius: 0px;
        overflow: hidden;
        border: 0px;
    }

    p {
        margin: 0;
        line-height: 1.2em;
    }

    /* No margin, uses area's padding instead. Between paragraphs, insert a new margin.*/
    p + p {
        margin-block-start: 0.5em;
    }

    > .header {
        grid-area: header;
        grid-template-areas:
            "name rarity";
        grid-template-columns: 80% 20%;
        grid-template-rows: 100%;
        border-bottom: 5px solid black;
        gap: 0px;
        padding: 0px;

        > .name {
            grid-area: name;
            font-size: 1.5em;
            border-right: 5px solid black;
            display: flex;
            align-items: center;
            padding: 5px;
        }

        > .rarity {
            grid-area: rarity;
            justify-self: center;
            height: 100%;
            width: auto;
        }
    }
    
    > .art {
        grid-area: art;
        padding: 0px;
        width: 100%;
        height: 100%; /* will contract just barely */
    }

    > .desc {
        grid-area: desc;
        padding: 5px;
        border-top: 5px solid black;
        border-bottom: 5px solid black;

        ul, ol { /* remove spacing before list */
            margin-block: 0;
        }

        svg.card-symbol {
            transform: translateY(0.2em); /* FIXME temporary fix to re-align symbols inside card descriptions */
        }
    }

    > .footer {
        grid-template-areas:
            "attack center health";
        grid-template-columns: 33% 34% 33%;
        grid-template-rows: 100%;

        align-items: center;
        justify-items: center;

        > .attack {
            grid-area: attack;
            font-size: 2em;
            display: flex;
            align-items: center;
        }

        > .center {
            grid-area: center;
            display: flex;
            align-items: center;
        }

        > .health {
            grid-area: health;
            font-size: 2em;
            display: flex;
            align-items:center;
        }
    }

    svg.card-symbol {
        display: inline-flex;
        align-self: center;
        width: 1.25em;
        height: 1.25em;
    }
}

.symbol-color {
    fill: var(--icon-color);
}

.card-display {
    padding: 5px;
    display: inline-block;
}

Google chart YAxis height and format

I am rendering actual vs budget hours graph per department per line item to show project performance. I have created a JSfiddle demo with complete working example.

My issue is that for several line items only few departments are needed but Google chart still allocates lot of space for those line items and makes chart bloated.

Any pointers or help will be greatly appreciated.

image showing issue

Example to show the issue:
https://jsfiddle.net/u0vrodcn

What I have done so far:

google.charts.load('current', { packages: ['corechart', 'bar'] })
google.charts.setOnLoadCallback(GenerateComplexBarChartReports)

// Helper function to find the minimum value for hAxis, if needed.
// This is a placeholder; you'll need to implement your actual logic here.
function FindMinValue(data) {
  let minValue = 0 // Default to 0, or calculate based on your data
  // Example: iterate through data to find min value if needed
  // for (let i = 0; i < data.getNumberOfRows(); i++) {
  //     for (let j = 1; j < data.getNumberOfColumns(); j += 8) { // Adjusted to jump by 8 for each department
  //         let actualHours = data.getValue(i, j);
  //         let budgetHours = data.getValue(i, j + 4); // Budget hours are 4 columns after actual
  //         if (typeof actualHours === "number" && actualHours < minValue) {
  //             minValue = actualHours;
  //         }
  //         if (typeof budgetHours === "number" && budgetHours < minValue) {
  //             minValue = budgetHours;
  //         }
  //     }
  // }
  return minValue
}

/**
 * Calculates the appropriate chart height based on the number of bars
 * per y-axis item to prevent overlap and ensure proper spacing.
 * @param {google.visualization.DataTable} data The DataTable object for the chart.
 * @returns {number} The calculated height for the chartArea.
 */
function CalculateChartHeightBasedOnDataItems(data) {
  // Height needed for a single bar including its annotation.
  // This is the fundamental unit of vertical space.
  const heightOfOneBarAndAnnotation = 30 // Further reduced for tighter packing

  // Minimal vertical space for the category label itself, if no bars are present,
  // or as a base for very sparse categories.
  const minHeightForCategoryLabel = 15 // Further reduced for tighter packing

  // Small, consistent padding *between* different y-axis categories.
  const interCategorySpacing = 5 // Further reduced for tighter packing

  let totalChartAreaHeight = 0

  for (let i = 0; i < data.getNumberOfRows(); i++) {
    let numberOfActiveSeriesInThisCategory = 0
    // Iterate through columns to count actual data series (bars) present for this row.
    // Your data structure is: PrimaryKey, SoftwareHours, SoftwareAnnotation, SoftwareToolTip, SoftwareColor,
    // SoftwareBudgetHours, SoftwareBudgetAnnotation, SoftwareBudgetToolTip, SoftwareBudgetColor,
    // ManufacturingHours, ... etc.
    // Each department block is 8 columns wide (value, annotation, tooltip, style, budget_value, budget_annotation, budget_tooltip, budget_style)
    // The first data column is at index 1 (SoftwareHours).
    for (let j = 1; j < data.getNumberOfColumns(); j += 8) {
      // Check for Actual Hours (column 'j')
      let actualHours = data.getValue(i, j)
      if (typeof actualHours === 'number' && actualHours > 0) {
        numberOfActiveSeriesInThisCategory++
      }

      // Check for Budget Hours (column 'j + 4' relative to the start of the department's block)
      let budgetHoursColIndex = j + 4
      // Ensure the budgetHoursColIndex is within the bounds of the data table's columns
      if (budgetHoursColIndex < data.getNumberOfColumns()) {
        let budgetHours = data.getValue(i, budgetHoursColIndex)
        if (typeof budgetHours === 'number' && budgetHours > 0) {
          numberOfActiveSeriesInThisCategory++
        }
      }
    }

    // Calculate height for the current y-axis category (row)
    if (numberOfActiveSeriesInThisCategory === 0) {
      // For categories with no bars, allocate minimal space for the label
      totalChartAreaHeight += minHeightForCategoryLabel + interCategorySpacing
    } else {
      // For categories with bars, calculate space based on active bars + inter-category spacing
      totalChartAreaHeight +=
        numberOfActiveSeriesInThisCategory * heightOfOneBarAndAnnotation +
        interCategorySpacing
    }
  }

  // Add some additional padding for the overall chart area,
  // useful for top/bottom margins within the plotting region.
  totalChartAreaHeight += 50 // General padding for the chart area itself

  return totalChartAreaHeight
}

/**
 * Generates the complex bar chart reports by iterating through elements
 * with the 'complex-barchart' class.
 */
function GenerateComplexBarChartReports() {
  let $jsonValueContainers = $('.complex-barchart')
  if ($jsonValueContainers.length <= 0) {
    console.log("No elements with class 'complex-barchart' found.")
    return
  }

  for (var i = 0; i < $jsonValueContainers.length; i++) {
    let $element = $($jsonValueContainers[i])
    let data = new google.visualization.DataTable($element.data('json'))
    let chartType = $element.data('chart-type')
    if (typeof chartType === 'undefined') {
      chartType = 'bar'
    }

    let hAxisMinValue = $element.data('h-axis-min-value')
    if (typeof hAxisMinValue === 'undefined') {
      // Call your FindMinValue function, or set a default
      hAxisMinValue = FindMinValue(data)
      $element.data('h-axis-min-value', hAxisMinValue)
    }

    let id = $element.attr('id')
    let chart = new google.visualization.BarChart(document.getElementById(id))

    // Calculate the height specifically for the chartArea
    let chartAreaCalculatedHeight = CalculateChartHeightBasedOnDataItems(data)

    // The overall chart height needs to be chartArea height PLUS space for hAxis, title, etc.
    // Adjust this buffer (e.g., 180) based on your chart's title, hAxis labels, and general padding needs.
    let overallChartHeight = chartAreaCalculatedHeight + 180 // Buffer for hAxis, title, etc.

    console.log(
      'Calculated Chart Area Height: ' + chartAreaCalculatedHeight + 'px',
    )
    console.log('Overall Chart Height: ' + overallChartHeight + 'px')

    let options = {
      annotations: {
        textStyle: {
          fontSize: 14          
        },
        // For bar charts, 'alwaysOutside: true' might not always place annotations outside the bar
        // if space is constrained. Google Charts tries its best.
        alwaysOutside: true,
      },
      vAxis: {
        textStyle: {
          fontSize: 14, // Font size for y-axis labels          
        },
        // You can add more vAxis options here for label positioning if needed
        // textPosition: 'out' // Example: position labels outside the chart area
      },
      tooltip: {
        isHtml: true,
        textStyle: {
          fontSize: 14,
        },
      },
      bar: { groupWidth: '75%' }, // Controls the thickness of the bars
      height: overallChartHeight, // Total height of the chart container
      chartArea: {
        height: chartAreaCalculatedHeight, // Height of the actual plotting area for bars
        left: 180, // Increased to give more room for long y-axis labels
        width: '65%', // Adjusted width of the chart area as a percentage
      },
      legend: { position: 'none' }, // No legend displayed
      bars: 'horizontal', // Required for Material Bar Charts to be horizontal
      // For Material charts, the hAxis is actually the x-axis for horizontal bars
      hAxis: {
        minValue: hAxisMinValue,
        textStyle: {
          fontSize: 12
        },
      },
    }

    // Conditionally add minValue to hAxis if hAxisMinValue is not 0
    if (hAxisMinValue !== 0) {
      options.hAxis.minValue = hAxisMinValue
    }

    chart.draw(data, options)
  }
}
body {
            font-family: 'Inter', sans-serif;
            display: flex;
            justify-content: center;
            align-items: flex-start;
            min-height: 100vh;
            background-color: #f0f2f5;
            padding: 20px;
            box-sizing: border-box;
        }
        .chart-container {
            width: 90%; /* Responsive width */
            max-width: 1200px; /* Max width for larger screens */
            background-color: #ffffff;
            border-radius: 12px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            padding: 20px;
            box-sizing: border-box;
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 20px;
        }
        /* Ensure the chart div itself can grow */
        #chart_div {
            width: 100%;
            /* Height will be set dynamically by Google Charts */
        }
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div class="chart-container">
        <h1>Dynamic Bar Chart Report</h1>
        <!-- The data-json attribute holds your chart data.
             It's structured as [label, value1, tooltip1, color1, value2, tooltip2, color2, ...]
             This mock data simulates the structure from your original image. -->
        <div id="chart_div" class="complex-barchart"
             data-chart-type="bar"
             data-json='{"cols": [{"type": "string" ,"id": "PrimaryKey" ,"label": "PrimaryKey" }, {"type": "number" ,"id": "SoftwareHours" ,"label": "Software" }, {"type": "string" ,"id": "SoftwareAnnotation" ,"label": "SoftwareAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "SoftwareToolTip" ,"label": "SoftwareToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "SoftwareColor" ,"label": "SoftwareColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "SoftwareBudgetHours" ,"label": "SoftwareBudgetHours" }, {"type": "string" ,"id": "SoftwareBudgetAnnotation" ,"label": "SoftwareBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "SoftwareBudgetToolTip" ,"label": "SoftwareBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "SoftwareBudgetColor" ,"label": "SoftwareBudgetColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "ManufacturingHours" ,"label": "Manufacturing" }, {"type": "string" ,"id": "ManufacturingAnnotation" ,"label": "ManufacturingAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "ManufacturingToolTip" ,"label": "ManufacturingToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "ManufacturingColor" ,"label": "ManufacturingColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "ManufacturingBudgetHours" ,"label": "ManufacturingBudgetHours" }, {"type": "string" ,"id": "ManufacturingBudgetAnnotation" ,"label": "ManufacturingBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "ManufacturingBudgetToolTip" ,"label": "ManufacturingBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "ManufacturingBudgetColor" ,"label": "ManufacturingBudgetColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "Electrical_ENGHours" ,"label": "Electrical_ENG" }, {"type": "string" ,"id": "Electrical_ENGAnnotation" ,"label": "Electrical_ENGAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "Electrical_ENGToolTip" ,"label": "Electrical_ENGToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "Electrical_ENGColor" ,"label": "Electrical_ENGColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "Electrical_ENGBudgetHours" ,"label": "Electrical_ENGBudgetHours" }, {"type": "string" ,"id": "Electrical_ENGBudgetAnnotation" ,"label": "Electrical_ENGBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "Electrical_ENGBudgetToolTip" ,"label": "Electrical_ENGBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "Electrical_ENGBudgetColor" ,"label": "Electrical_ENGBudgetColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "Mechanical_ENGHours" ,"label": "Mechanical_ENG" }, {"type": "string" ,"id": "Mechanical_ENGAnnotation" ,"label": "Mechanical_ENGAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "Mechanical_ENGToolTip" ,"label": "Mechanical_ENGToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "Mechanical_ENGColor" ,"label": "Mechanical_ENGColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "Mechanical_ENGBudgetHours" ,"label": "Mechanical_ENGBudgetHours" }, {"type": "string" ,"id": "Mechanical_ENGBudgetAnnotation" ,"label": "Mechanical_ENGBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "Mechanical_ENGBudgetToolTip" ,"label": "Mechanical_ENGBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "Mechanical_ENGBudgetColor" ,"label": "Mechanical_ENGBudgetColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "ProjectManagementHours" ,"label": "ProjectManagement" }, {"type": "string" ,"id": "ProjectManagementAnnotation" ,"label": "ProjectManagementAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "ProjectManagementToolTip" ,"label": "ProjectManagementToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "ProjectManagementColor" ,"label": "ProjectManagementColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "ProjectManagementBudgetHours" ,"label": "ProjectManagementBudgetHours" }, {"type": "string" ,"id": "ProjectManagementBudgetAnnotation" ,"label": "ProjectManagementBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "ProjectManagementBudgetToolTip" ,"label": "ProjectManagementBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "ProjectManagementBudgetColor" ,"label": "ProjectManagementBudgetColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "AdminHours" ,"label": "Admin" }, {"type": "string" ,"id": "AdminAnnotation" ,"label": "AdminAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "AdminToolTip" ,"label": "AdminToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "AdminColor" ,"label": "AdminColor" , "p": {"role" : "style"}}, {"type": "number" ,"id": "AdminBudgetHours" ,"label": "AdminBudgetHours" }, {"type": "string" ,"id": "AdminBudgetAnnotation" ,"label": "AdminBudgetAnnotation" , "p": {"role" : "annotation"}}, {"type": "string" ,"id": "AdminBudgetToolTip" ,"label": "AdminBudgetToolTip" , "p": {"role" : "tooltip"}}, {"type": "string" ,"id": "AdminBudgetColor" ,"label": "AdminBudgetColor" , "p": {"role" : "style"}}], "rows" : [{"c" : [{"v": "DR (OCS) &amp; PASS (ODS)"}, {"v": 660.00}, {"v": "Manufacturing Actual: 660.00hrs (85.3%); Left: 98.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Manufacturing Actual: 660.00hrs (85.3%); Left: 98.00hrs"}, {"v": "#A9D08E"}, {"v": 758.00}, {"v": "Manufacturing Budget: 758.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Manufacturing Actual: 660.00hrs (85.3%); Left: 98.00hrs"}, {"v": "{fill-color:#A9D08E;fill-opacity: 0.5;}"}, {"v": 35.25}, {"v": "Software Actual: 35.25hrs (4.6%); Left: 144.75hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Software Actual: 35.25hrs (4.6%); Left: 144.75hrs"}, {"v": "#7030A0"}, {"v": 180.00}, {"v": "Software Budget: 180.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Software Actual: 35.25hrs (4.6%); Left: 144.75hrs"}, {"v": "{fill-color:#7030A0;fill-opacity: 0.5;}"}, {"v": 73.75}, {"v": "Electrical_ENG Actual: 73.75hrs (9.5%); Left: 14.25hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Electrical_ENG Actual: 73.75hrs (9.5%); Left: 14.25hrs"}, {"v": "#FFFF00"}, {"v": 88.00}, {"v": "Electrical_ENG Budget: 88.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Electrical_ENG Actual: 73.75hrs (9.5%); Left: 14.25hrs"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}, {"v": 5.00}, {"v": "Mechanical_ENG Actual: 5.00hrs (0.6%); Left: 103.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Mechanical_ENG Actual: 5.00hrs (0.6%); Left: 103.00hrs"}, {"v": "#305496"}, {"v": 108.00}, {"v": "Mechanical_ENG Budget: 108.00hrs"}, {"v": "DR (OCS) &amp; PASS (ODS) Mechanical_ENG Actual: 5.00hrs (0.6%); Left: 103.00hrs"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "EOL"}, {"v": 181.50}, {"v": "Manufacturing Actual: 181.50hrs (31.9%); Left: 326.50hrs"}, {"v": "EOL Manufacturing Actual: 181.50hrs (31.9%); Left: 326.50hrs"}, {"v": "#A9D08E"}, {"v": 508.00}, {"v": "Manufacturing Budget: 508.00hrs"}, {"v": "EOL Manufacturing Actual: 181.50hrs (31.9%); Left: 326.50hrs"}, {"v": "{fill-color:#A9D08E;fill-opacity: 0.5;}"}, {"v": 220.00}, {"v": "Electrical_ENG Actual: 220.00hrs (38.7%); OverBudget: 82.00hrs (By: 59.42%)"}, {"v": "EOL Electrical_ENG Actual: 220.00hrs (38.7%); OverBudget: 82.00hrs (By: 59.42%)"}, {"v": "#FFFF00"}, {"v": 138.00}, {"v": "Electrical_ENG Budget: 138.00hrs"}, {"v": "EOL Electrical_ENG Actual: 220.00hrs (38.7%); OverBudget: 82.00hrs (By: 59.42%)"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}, {"v": 142.00}, {"v": "Software Actual: 142.00hrs (25.0%); Left: 37.25hrs"}, {"v": "EOL Software Actual: 142.00hrs (25.0%); Left: 37.25hrs"}, {"v": "#7030A0"}, {"v": 179.25}, {"v": "Software Budget: 179.25hrs"}, {"v": "EOL Software Actual: 142.00hrs (25.0%); Left: 37.25hrs"}, {"v": "{fill-color:#7030A0;fill-opacity: 0.5;}"}, {"v": 24.75}, {"v": "Mechanical_ENG Actual: 24.75hrs (4.4%); Left: 5.25hrs"}, {"v": "EOL Mechanical_ENG Actual: 24.75hrs (4.4%); Left: 5.25hrs"}, {"v": "#305496"}, {"v": 30.00}, {"v": "Mechanical_ENG Budget: 30.00hrs"}, {"v": "EOL Mechanical_ENG Actual: 24.75hrs (4.4%); Left: 5.25hrs"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "Roll cart and Lift &amp; Locate"}, {"v": 83.75}, {"v": "Mechanical_ENG Actual: 83.75hrs (85.9%); OverBudget: 45.75hrs (By: 120.39%)"}, {"v": "Roll cart and Lift &amp; Locate Mechanical_ENG Actual: 83.75hrs (85.9%); OverBudget: 45.75hrs (By: 120.39%)"}, {"v": "#305496"}, {"v": 38.00}, {"v": "Mechanical_ENG Budget: 38.00hrs"}, {"v": "Roll cart and Lift &amp; Locate Mechanical_ENG Actual: 83.75hrs (85.9%); OverBudget: 45.75hrs (By: 120.39%)"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}, {"v": 13.75}, {"v": "Manufacturing Actual: 13.75hrs (14.1%); Left: 3.25hrs"}, {"v": "Roll cart and Lift &amp; Locate Manufacturing Actual: 13.75hrs (14.1%); Left: 3.25hrs"}, {"v": "#A9D08E"}, {"v": 17.00}, {"v": "Manufacturing Budget: 17.00hrs"}, {"v": "Roll cart and Lift &amp; Locate Manufacturing Actual: 13.75hrs (14.1%); Left: 3.25hrs"}, {"v": "{fill-color:#A9D08E;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "AC and PB"}, {"v": 24.50}, {"v": "Mechanical_ENG Actual: 24.50hrs (88.3%); Left: 30.00hrs"}, {"v": "AC and PB Mechanical_ENG Actual: 24.50hrs (88.3%); Left: 30.00hrs"}, {"v": "#305496"}, {"v": 54.50}, {"v": "Mechanical_ENG Budget: 54.50hrs"}, {"v": "AC and PB Mechanical_ENG Actual: 24.50hrs (88.3%); Left: 30.00hrs"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}, {"v": 3.25}, {"v": "Electrical_ENG Actual: 3.25hrs (11.7%); Left: 14.00hrs"}, {"v": "AC and PB Electrical_ENG Actual: 3.25hrs (11.7%); Left: 14.00hrs"}, {"v": "#FFFF00"}, {"v": 17.25}, {"v": "Electrical_ENG Budget: 17.25hrs"}, {"v": "AC and PB Electrical_ENG Actual: 3.25hrs (11.7%); Left: 14.00hrs"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "Cal Verify &amp; Cert Plate"}, {"v": 16.00}, {"v": "Mechanical_ENG Actual: 16.00hrs (94.1%); Left: 11.00hrs"}, {"v": "Cal Verify &amp; Cert Plate Mechanical_ENG Actual: 16.00hrs (94.1%); Left: 11.00hrs"}, {"v": "#305496"}, {"v": 27.00}, {"v": "Mechanical_ENG Budget: 27.00hrs"}, {"v": "Cal Verify &amp; Cert Plate Mechanical_ENG Actual: 16.00hrs (94.1%); Left: 11.00hrs"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}, {"v": 1.00}, {"v": "Electrical_ENG Actual: 1.00hrs (5.9%); Left: 4.00hrs"}, {"v": "Cal Verify &amp; Cert Plate Electrical_ENG Actual: 1.00hrs (5.9%); Left: 4.00hrs"}, {"v": "#FFFF00"}, {"v": 5.00}, {"v": "Electrical_ENG Budget: 5.00hrs"}, {"v": "Cal Verify &amp; Cert Plate Electrical_ENG Actual: 1.00hrs (5.9%); Left: 4.00hrs"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "63-Way Cables"}, {"v": 28.25}, {"v": "Manufacturing Actual: 28.25hrs (60.4%); Left: 16.25hrs"}, {"v": "63-Way Cables Manufacturing Actual: 28.25hrs (60.4%); Left: 16.25hrs"}, {"v": "#A9D08E"}, {"v": 44.50}, {"v": "Manufacturing Budget: 44.50hrs"}, {"v": "63-Way Cables Manufacturing Actual: 28.25hrs (60.4%); Left: 16.25hrs"}, {"v": "{fill-color:#A9D08E;fill-opacity: 0.5;}"}, {"v": 15.00}, {"v": "Mechanical_ENG Actual: 15.00hrs (32.1%); Left: 13.00hrs"}, {"v": "63-Way Cables Mechanical_ENG Actual: 15.00hrs (32.1%); Left: 13.00hrs"}, {"v": "#305496"}, {"v": 28.00}, {"v": "Mechanical_ENG Budget: 28.00hrs"}, {"v": "63-Way Cables Mechanical_ENG Actual: 15.00hrs (32.1%); Left: 13.00hrs"}, {"v": "{fill-color:#305496;fill-opacity: 0.5;}"}, {"v": 3.50}, {"v": "Electrical_ENG Actual: 3.50hrs (7.5%); Left: 12.50hrs"}, {"v": "63-Way Cables Electrical_ENG Actual: 3.50hrs (7.5%); Left: 12.50hrs"}, {"v": "#FFFF00"}, {"v": 16.00}, {"v": "Electrical_ENG Budget: 16.00hrs"}, {"v": "63-Way Cables Electrical_ENG Actual: 3.50hrs (7.5%); Left: 12.50hrs"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "ProjectManagement"}, {"v": 24.25}, {"v": "ProjectManagement Actual: 24.25hrs (100.0%); Left: 65.75hrs"}, {"v": "ProjectManagement ProjectManagement Actual: 24.25hrs (100.0%); Left: 65.75hrs"}, {"v": "#ED7D31"}, {"v": 90.00}, {"v": "ProjectManagement Budget: 90.00hrs"}, {"v": "ProjectManagement ProjectManagement Actual: 24.25hrs (100.0%); Left: 65.75hrs"}, {"v": "{fill-color:#ED7D31;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "Red Rabbit Box"}, {"v": 1.00}, {"v": "Electrical_ENG Actual: 1.00hrs (100.0%); Left: 5.00hrs"}, {"v": "Red Rabbit Box Electrical_ENG Actual: 1.00hrs (100.0%); Left: 5.00hrs"}, {"v": "#FFFF00"}, {"v": 6.00}, {"v": "Electrical_ENG Budget: 6.00hrs"}, {"v": "Red Rabbit Box Electrical_ENG Actual: 1.00hrs (100.0%); Left: 5.00hrs"}, {"v": "{fill-color:#FFFF00;fill-opacity: 0.5;}"}]}, {"c" : [{"v": "Admin"}, {"v": 0.50}, {"v": "Admin Actual: 0.50hrs (100.0%); Left: 56.00hrs"}, {"v": "Admin Admin Actual: 0.50hrs (100.0%); Left: 56.00hrs"}, {"v": "#ED7D31"}, {"v": 56.50}, {"v": "Admin Budget: 56.50hrs"}, {"v": "Admin Admin Actual: 0.50hrs (100.0%); Left: 56.00hrs"}, {"v": "{fill-color:#ED7D31;fill-opacity: 0.5;}"}]}]}'
             data-h-axis-min-value="0">
        </div>
    </div>

Virtual-scrolling works with native scrollbar but not with SimpleBar—rows appear but list won’t scroll

I’m trying to add virtual scrolling to a long list that uses SimpleBar for custom styling.
With the native scrollbar the technique works: I create a very tall invisible spacer (.sizer) to give the scroll-bar its full range and then absolutely-position only the visible rows inside the scroll container.
When I switch to SimpleBar the rows render, yet I cannot scroll beyond the first few items.

Native version (works):

  <div class="sizer"></div>   <!-- height = 30 px * 1 000 000 -->
  <!-- visible rows are appended here -->
</div>

<script>
const ROW_HEIGHT  = 30;
const TOTAL_ITEMS = 1_000_000;

const viewport = document.getElementById('viewport');
function render () {
  const scrollTop = viewport.scrollTop;
  /* …compute first/last, paint rows… */
}
viewport.addEventListener('scroll', render);
render();
</script>

SimpleBar version (rows appear, no scrolling):

<div id="viewport" data-simplebar>
  <div class="sizer"></div>
</div>
<script>
const sb  = new SimpleBar(document.getElementById('viewport'));
const scrollEl = sb.getScrollElement();   // .simplebar-content-wrapper
function render () {
  const scrollTop = scrollEl.scrollTop;
  /* …compute first/last, paint rows inside sb.getContentElement() … */
}
scrollEl.addEventListener('scroll', render);
render();
</script>

The spacer (.sizer) is inside the SimpleBar content element, yet dragging the thumb only moves a few pixels and the list never reaches the bottom.
What am I missing so that SimpleBar respects the spacer’s height and allows full scrolling?

Full Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <title>Virtual scroll + SimpleBar</title>

  <!-- SimpleBar CSS -->
  <link rel="stylesheet" href="simplebar.min.css"/>
  <style>
    #viewport {
      height: 400px;
      width: 300px;
      border: 1px solid #444;
      /* SimpleBar will turn this into a scrollable widget */
    }

    /* invisible spacer that gives the scrollbar its full range */
    .sizer {
      height: calc(30px * 1000000);
    }

    /* rows are absolutely positioned inside the simplebar-content area */
    .row {
      position: absolute;
      left: 0;
      width: 100%;
      height: 30px;
      line-height: 30px;
      box-sizing: border-box;
      border-bottom: 1px solid #ddd;
    }
  </style>
</head>

<body>
  <h2>Virtual list (1 000 000 rows) with SimpleBar</h2>
  <div id="viewport" data-simplebar>
    <div class="sizer"></div>
  </div>

  <!-- SimpleBar JS -->
  <script src="simplebar.min.js"></script>
  <script>
    const ROW_HEIGHT  = 30;
    const TOTAL_ITEMS = 1_000_000;
    const BUFFER      = 5;

    // SimpleBar gives us the *real* scrollable wrapper
    const sb       = new SimpleBar(document.getElementById('viewport'));
    const scrollEl = sb.getScrollElement();      // the <div class="simplebar-content-wrapper">

    function render() {
      const scrollTop = scrollEl.scrollTop;
      const first     = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER);
      const last      = Math.min(TOTAL_ITEMS - 1,
                                 Math.ceil((scrollTop + scrollEl.clientHeight) / ROW_HEIGHT) + BUFFER);

      /* clear old rows */
      sb.getContentElement().querySelectorAll('.row').forEach(r => r.remove());

      /* add visible rows */
      for (let i = first; i <= last; i++) {
        const div = document.createElement('div');
        div.className = 'row';
        div.style.top  = i * ROW_HEIGHT + 'px';
        div.textContent = `Row #${i + 1}`;
        sb.getContentElement().appendChild(div);
      }
    }

    scrollEl.addEventListener('scroll', render, { passive: true });
    render();        // initial draw
  </script>
</body>
</html>

How to set custom validation message for HTML input on mouseenter?

I have ASP.NET Core project and trying to set custom validation message for HTML form input field on mouseenter. I tried to add event listeners for input, change and mouseenter events like this.

<!DOCTYPE html>
<html>
<body>
    <form>
        <label for="field1">Field1:</label>
        <input id="field1" type="text" class="validate" data-error-message="You must fill in field1!" required
               oninput="this.setCustomValidity('');this.reportValidity()" onchange="validateInputElement(this)" onmouseenter="validateInputElement(this)" />
        <label for="field2">Field1:</label>
        <input id="field2" type="text" class="validate" data-error-message="You forgot to fill in field2!" required
               oninput="this.setCustomValidity('');this.reportValidity()" onchange="validateInputElement(this)" onmouseenter="validateInputElement(this)" />
    </form>
    <script>
        function validateInputElement(element)
        {
            let validationMessage = "";
            if(element.validity !== ValidityState.valid)
            {
                validationMessage = element.dataset["errorMessage"];
            }
            else if(document.querySelector("input#field1")?.value === document.querySelector("input#field2")?.value)
            {
                validationMessage = "You must enter different values!"
            }
            element.setCustomValidity(validationMessage);
            element.reportValidity();
        }
    </script>
</body>
</html>

In this case, my custom validation messages are displayed, but the default validation messages are also displayed. How to disable default validation messages?

Jquery DatetimePicker Selected Datetime automatically shifting Issue

I have configured the jquery datetimepicker by cdn. The datetimepicker have shifting the time 1 hour back from the input field value for each time opened the datetimepicker.

Given below the code

  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script>

<input required="required" name="shift-datetime" id="shift-datetime" value="Jul 14, 2025 07:30 PM" class="form-control" autocomplete="off">

  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"></script>

<input required="required" name="shift-datetime" id="shift-datetime" value="Jul 14, 2025 07:30 PM" class="form-control" autocomplete="off">

<script>
    $("#shift-datetime").datetimepicker({
    format: "M d, Y h:i A",
    formatTime: "h:i A",
    step: 30,
    timepicker: true,
    hours12: false,
    defaultTime: false,
    seconds: true
    });
    </script>

onclick event not firing under any circumstance, but worked earlier in document

I have an onclick event that should fire a function to pull data from a form and calculate a z score. The previous onclick usage worked to write a random number to a label, but this one won’t even show the alert that pulls the value from one of the form fields. Any ideas?

<form>
   <label>Random Number</label>
   <label id="rannum"></label>
   <script>
      function writeRand(){
         let surprise = Math.floor(Math.random() * 100) + 1;
          document.getElementById("rannum").innerHTML='Random number: ' + surprise;
         }
   </script>
   <br />
   <button type="button" onclick="writeRand()">Get Random Number</button>
</form>
</section>

<!--Section to calcculate Z-Score based on user input-->
<section name="ZScoreCalc">
   <p>Calculate Z Score</p>
   <form>
      <label>X</label>
      <input type="number" id="setX"></input><br>
      <label>Sample Mean</label><br>
      <input type="number" id="splMean"></input><br>
      <label>Standard Deviation</label><br>
      <input type="number" id="stdDev"></input><br>
      <button type="button" id="CalcBtn" onmousedown="findZscore()">Calculate Z</button>
      <label>Z Score is:</label>
      <label id="zScore"></label>
      <script>
         function findZscore(){
             let ex=document.getElementById("setX").value;
             let mn=documment.getElementById("splMean").value;
             let sd=document.getElementById("stdDev").value;
         
             let z=(ex-mn)/sd;
             window.alert(document.getElementById("setX").value);
             window.alert(mn);
             window.alert(sd);
             window.alert(z);
             
             document.getElementById("zScore").value=z;
         }
      </script>
   </form>
</section>

I tried putting a listener event in the script to catch the button click. As you can see in the code, I also tried capturing it as a mousedown event. Nothing seems to fire the function.

Why does my JWT-based session not auto-expire after 2 hours in a MERN HRMS project?

I’m building an HRMS dashboard using the MERN stack as part of a hiring test. I’m using JWT for authentication. I want the user to be automatically logged out after 2 hours.

Here’s what I did in the backend:

js
Copy
Edit
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: ‘2h’,
});
On the frontend (React), I store the token in localStorage and attach it to headers for API calls.

js
Copy
Edit
localStorage.setItem(“token”, res.data.token);
But the token still works even after 2 hours unless I manually delete it.
I expected the session to auto-expire and log out the user.

What I want:
Auto-logout after 2 hours.

Either on token expiry or by checking on each route.

What I tried:
Checked token expiry with jwt.decode().

Manually clearing token after setTimeout (not ideal).

Not sure how to sync frontend with token expiry.

How can I correctly implement JWT auto-expiry and frontend logout when the token is expired?

Why should we use enum in TS instant of object?

to achieve enum in js we use Objects. it will the same impact as enum. Even though we have objects in TS we are using enum instant of object so what will be the difference of enum and object?

let HttpStatusCode = {
OK : 200,
INTERNAL_SERVER_ERROR : 500,
NOT_FOUND : 404
}

this is js

enum HttpStatusCode {
  OK = 200,
  NOT_FOUND = 404,
  INTERNAL_SERVER_ERROR = 500
}

this is TS so whats the difference