Use JavaScript/HTML/CSS to Build a Dynamic Line Chart!

Use JavaScript/HTML/CSS to Build a Dynamic Line Chart!

As is the case with any type of coding language, there will always be multiple ways to solve any problem or challenge. The one I used took advantage of both HTML and CSS in conjunction with JS to build the data points and lines of the chart. JS was the tool used to pull data from an API or database and append it to HTML tags. Above you can see a screenshot of the line chart I made to track stock history of a particular stock and it is updated every five minutes.

HTML

Let's first start by laying the foundation for the chart. I started by using a simple <ul> wrapped in a <figure>:

<h3></h3>
<figure id="chart">
    <ul class="lineChart">
    </ul>
</figure>

Good news, this is all the actual HTML we will write! We will primarily use JS to pull the data from an API or database and create elements that we will append to the starting HTML structure above.

JavaScript

Next, we will write the function for the fetch that will get the information from the API/database of your choice. In my example, I will be designing a line chart to show stock value over the last 24 hours.

function fetchStockAction(stockName) {
    fetch(`/*link of your API*//${stockName}`)
        .then(resp => resp.json())
        .then(data => displayChart(data.prices));
}

Make sure you are using the proper format for inserting the id into the API URL for whatever data you are grabbing. In my case, the data have ids in the form of strings (specifically the stock names) instead of numbers like most other APIs. I used data.prices as the return instead of data because of the way the API I was using had the data organized. It was in such a way that the first property (prices) had an array of arrays that include the price of the stock every hour for the past 24 hours. Next, we will begin to write the displayChart() function that was called in the .then() above:

function displayChart(stockAction) {
    const change = stockAction[24][1] - stockAction[0][1];
    const percChange = change/stockAction[24][1];
    let changeVal = change.toFixed(4).toString();
    let changeSign = '';
    let lineColor = 'rgb(60, 255, 60)';
    if (change < 0) {
        changeVal = changeVal.slice(1);
        changeSign = '-';
        lineColor = 'red';
    }
    const h3 = document.querySelector('h3');
    h3.textContent = `24 hour change: ${changeSign}$${changeVal} (${percChange.toFixed(4)}%)`
    ...

This portion initializes most of the variables needed later on and also determines the 24-hour percent and dollar change of the stock (final minus initial price) and places them into the <h3> right above the chart. This part also sets a variable for the color of the line graph and points which will either be red or green based on whether the 24-hour price change is negative or positive. Next, I need to grab only the dollar values from each of the arrays within data.prices (aka stockAction within this function) array. This may not be something you need to do depending on the database you are using. Next, I defined a couple more variables before going into the meat of building the chart:

    ...
    const prices = [];
    stockAction.forEach(elWithPrice => {
        prices.push(elWithPrice[1]);
    })
    const ul = document.querySelector('ul');
    ul.innerHTML = '';
    const max = Math.max(...prices);
    const min = Math.min(...prices);
    let oldrise = 0;
    let i = 0;
    ...

I simply grabbed the second element in each array of the array stockAction. This allowed me to find the maximum and minimum prices in the last 24 hours (which will be useful in helping us determine the scale of the y-axis). Then, I grabbed the ul element from within the HTML and cleared all of its innerHTML. This may not do anything the first time, but on subsequent calls of the fetch function for other stocks, I needed the previous data to clear from the screen, and this is how I did that. The oldrise variable is to help keep track of the y value of the previous data point to help find the slope of the line later. The i variable is just for counting which data point we are on to multiply it and find our x value. Before this part, know that the left and bottom borders of the ul will be the axes (with a thickness of 1px), the length of the y-axis will be 400px, and the length of the x-axis will be 1000px. Next, I started the construction of the chart:

    ...
    stockAction.forEach(price => {
        const li = document.createElement('li');
        let newrise = ((price[1] - min)/(max-min))*400;
        li.style = `--y: ${newrise}px; --x: ${40*i}px;`;
        const hyp = Math.sqrt(((newrise-oldrise)**2)+(40**2));
        let ang = 0;
        if (newrise > oldrise) {
            ang = Math.asin(40/hyp) + Math.PI/2;
        } else {
            ang = Math.asin((oldrise-newrise)/hyp) + Math.PI;
        }
        li.innerHTML = `<div class="dataPoint" value="${price[1]}" style="border: 2px solid ${lineColor}; cursor: pointer;";></div>
        <div class="lineSegment" style="--hypotenuse: ${hyp}; --angle: ${ang}; background-color: ${lineColor};"></div>`;
        ul.append(li);
        ...

This is where I create each <li> within the <ul> with attributes that will contain information pertinent to defining the slope of each line, the length of each line, the y-value of each point, and the x-value of each point. I started by calling a .forEach() on the stockAction array and defined each element as price. For each of the prices, I created an <li>. Then, I defined newrise to be the difference between the current price and the minimum and divided it by the difference between the min and max and finally multiplied it by the length of the y-axis (400px). Next, I gave it a style of --y and --x where --y is just the current newrise and the --x is the x-axis length (1000px) divided by 25 (yielding 40) and then multiplied by the current i value. Then I calculated the length using the Pythagorean theorem rearranged to solve for the hypotenuse. Then, I assigned ang to be 0 and made a condition that compared the oldrise and newrise values to determine the angle by using arcsin. Finally, I injected all this information into the innerHTML of the <li> as two <div>s (one as the datapoint and the other the line) and then appended the <li> to the <ul>. Next, I design the tooltip to hover over each point with your mouse to show the dollar value of the price at that point:

        const tooltip = document.createElement('div');
        tooltip.className = 'tooltiptext';
        tooltip.textContent = `$${price[1].toFixed(4)}`;
        const datapoint = li.querySelector('div');
        datapoint.addEventListener('mouseover', () => {
            datapoint.appendChild(tooltip);
        });
        datapoint.addEventListener('mouseout', () => {
            document.querySelector('.tooltiptext').remove();
        });            
        oldrise = newrise;
        i ++;
    })
    document.getElementsByClassName('lineSegment')[0].remove();
}

As you can see, I created a div that will serve as the tooltip and gave it a class. The text content of the tooltip was a dollar sign followed by the value of the current price. After grabbing the newly made datapoint div with the variable datapoint, I added two event listeners to it. One is a mouseover that would display the tooltip, and the other is a mouseout that would hide the mouseover. For the final part of the .forEach(), I set oldrise to equal newrise so that when the loop goes to the next point, it can have access to the previous point's y value. Finally, I used .remove() on the first line created because it is erroneous; there is no point before the first.

CSS:

Next, I will be designing the visual of the chart using CSS:

#chart {
    border-bottom: 1px solid rgb(227, 233, 237);    /* x-axis */
    border-left: 1px solid rgb(227, 233, 237);     /* y-axis */
    height: 400px;    /* y-axis length */
    padding: 0;
    margin: 0;
    position: relative;
    width: 1000px;    /* x-axis length */
    z-index: 1;
}
.lineChart {
    list-style: none;    /* prevents chart from having eronneous bullets*/
    margin: 0;
    padding: 0;
    z-index: 1;
}
.dataPoint {
    background-color: white;
    border-radius: 50%;    /* to make a circular point */
    height: 12px;    /* data point height */
    position: absolute;    /* needed to make the points position correctly */
    width: 12px;    /* data point width */
    z-index: 1;
    bottom: calc(var(--y) - 8px);    /* explained below 1 */
    left: calc(var(--x) - 8px);    /* explained below 2 */
}
.lineSegment {
    bottom: var(--y);    /* line y-value origin */
    height: 3px;    /* line width */
    left: var(--x);    /* line x-value origin */
    position: absolute;
    width: calc(var(--hypotenuse) * 1px);    /* line length */
    transform: rotate(calc(var(--angle) * 1rad));    /* explained below 3 */
    transform-origin: left bottom;    /* explained below 4 */
}

This is the recommended CSS for the code I wrote earlier to show a well-designed and interactive line chart created from a <ul>. The important portions are pointed out and explained in the CSS except for the four more complicated ones. (1 and 2): This calculation is to account for the offset that the datapoint is not placed based on the center of the div but based on the corner. If you make the length and width of the point something other than 12px, you need to readjust the 8px that is being subtracted from the x and y values to get proper positioning. (3): This is the calculation needed to rotate the line so that it is at the correct angle based on what we calculated in the JS. (4): This is needed to ensure we are rotating the line from the origin (from each point) and not from the center of the line. Next, I will make the CSS for the tooltip:

.tooltiptext {
    width: 120px;
    background-color: rgba(0, 0, 0, 0.709);
    color: #fff;
    text-align: center;
    padding: 5px 0;
    border-radius: 6px;
    position: absolute;
    z-index: 4;
    width: 120px;
    bottom: 100%;    /* 1 */
    left: 50%;    /* 2 */
    margin-left: -10px;    /* 3 */
}

This is straightforward as it just describes the characteristics of the tooltip text box. You can change attributes 1, 2, or 3 if you would like to change the position of the tooltip relative to the data point when you hover over it (play around with it). Also, you can change the width and height depending on what you need to be written in the tooltip tip for your chart.

Conclusion

Before finishing up, the last thing you need to do is write the calls for the function that will trigger the beautiful you just finished writing:

setInterval(fetchStockAction(currentStock), 60*5*1000);

In my case, I want this code to run every 5 minutes because the API I used updated its information at that interval. To keep my chart looking alive, I just called the fetch function in a setInterval() set for every 5 minutes.

At this point, you should have a fully functioning interactive line chart on your webpage using, I think, a very fun and creative way to make a line chart. This challenge is important in testing one's ability to find synergistic ways for HTML, JS, and CSS to collaborate to uniquely solve complex problems. Upon completing this challenge you should have a lot more understanding in the interconnection between JS, HTML, and CSS to create very distinctive designs!

Some final thoughts about how you can make your chart better are by adding y-axis and x-axis labels and maybe even values that change as hours pass by. There are always ways to improve anything we design we just need to be creative and make an effort to put in the work!