DEV Community

Cover image for Stacked Bar Chart using a JSON Data Source, Plain Vanilla Javascript, Plain CSS and no chart libraries
Rick Delpo
Rick Delpo

Posted on • Updated on

Stacked Bar Chart using a JSON Data Source, Plain Vanilla Javascript, Plain CSS and no chart libraries

First, thank you Kevin at https://dev.to/kevtiq for getting us started on this bar chart. Here is what we are working on.

stacked bar chart

I have re formatted and populated Kevin's original chart with JSON data.

Here are some changes I made to his first pass.

  1. I put html code in a script so we could use a for loop because he repeats the same code many times. For javascript it is necessary to create a new html div on the fly in order for the loop to work in a script.
  2. I added grid lines to the background and used z-index in my css so lines appear behind the bars.
  3. I liked the way Kevin used nth child so I kept this and am using it for the 3 section children in each bar.
  4. I populated the stacked bars from a real json data source found in the program. Also optionally u can save this json file in AWS s3 and call it from there using the js fetch api. Currently I have this section commented out but it is there to show the user how to load from a remote source.
  5. I used mainly CSS and html to build the bars and used array.reduce on my original json array to sum up the product lines. Then I used moment.js to group the sums for each month found in the timestamps of the original data.

some issues
For those who have read this far into the article thanks for reading but I propose a challenge:

  1. I would like to know how to group by date without having to use Moment.js if anyone can help

update, see this link for the answer
https://dev.to/rickdelpo1/how-to-populate-a-stacked-bar-chart-in-plain-javascript-12p9

  1. also would like to group by product name instead of putting products in 3 data keys. If anyone can help with this see the 2 unused fields in my original json data array.

Feel free to copy and paste my entire code from codepen at https://codepen.io/rickdelpo/pen/yLRbEJR , or see below.
For beginners, copy and paste to notepad then save with .html extension and doubleclick to view. There are lots of comments in my code to guide the beginner. Please copy my code because it is also a tutorial. The code is loaded with useful, instructional comments.

<!DOCTYPE html>
<html>
  <!--example of stacked bar chart showing product sales over 5 months, 3 products in each bar-->
<head>
<meta charset="UTF-8">
     <!--use moment.js only for time series data, ie sales for jan, feb, mar etc-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>

<style>

    /* used below for section coloring, each section has 3 siblings*/
:nth-child(1) { --nth-child: 1 }
:nth-child(2) { --nth-child: 2 }
:nth-child(3) { --nth-child: 3 }

body {
  font-family: sans-serif;
}

.chart {
  width: 100%;
  max-width: 800px;
  height: 400px;
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  /*gap: 2px;*/
  gap: 12px; /*space between bars*/
  z-index: 1; /*this is so chart class overlaps barchart class so grid lines are behind the bars in the background*/

}

.bar {
  display: flex;
  flex-direction: column;
  height: var(--bar-height, 100%);
  flex-grow: 1;
  transition: all 300ms ease;

}

.section {
   /* border-bottom: 3px solid black; */
  display: flex;
  flex-grow: var(--section-value, 1); 
  /*transition: all 300ms ease;*/
      /*using nth child for 3 children sections of each bar*/
  background-color: hsl(calc(100 * var(--nth-child)) 100% 40%);
  /*background-color: hsl(calc(100 * var(--nth-child)) 80% 70%);*/
/*align-items: flex-end*/
}

.section::after {
  content: attr(data-value); //pulls in values for 3 products per bar
  /*opacity: 100;*/ /*was 0, meaning default val was hidden*/
  color: black; /*color of text*/
  margin: auto;
  text-align: center;
 /* transition: opacity 300ms ease; */
}

.label {
  text-align: center;
}


/* below is css for grid lines and Y axis*/

    .barchart-Wrapper {
      display: table;
      position: relative;
      margin: 20px 0;
      height: 252px;
    }

    .barChart-Container {
      display: table-cell;
      width: 100%;
      height: 100%;
      padding-left: 45px; /*width of y axis, this is where first bar begins*/ 
    }

          /*barchart class has gridlines*/
    .barchart {
     display: table;
     table-layout: fixed;
     height: 100%;
     width: 100%;
     /*border-bottom: 3px solid tomato;*/
    }

/* this is needed for bars to appear*/
    .barchart-Col {
      position: relative;
      vertical-align: bottom;
      display: table-cell;
      height: 100%;
    }

    .barchart-Col + .barchart-Col {
      border-left: 4px solid transparent;
    }

/* width and spacing of bars*/
    .barchart-Bar {
      position: relative;
      height: 0;
      transition: height 0.5s 2s;
      width: 66px;  /*actual bar width*/
      margin: auto;
    }

        /*needed for y axis spacing top to bottom */
    .barchart-YCol {    /* y axis was originally called the time column*/
      position: absolute;
      top: 0;
      height: 100%;
      width: 100%;
    }

         /* the y axis*/
    .barchart-Y {
      height: 25%; /*each grid is 25% of total height*/
      vertical-align: middle;
      position: relative;
    }

    .barchart-Y:after {
           /*this part does lines across = grid lines using css after selector*/
      /*border-bottom: 3px solid black;*/
border-bottom: 1px solid black; /*showing bottom border as a grid line after the time element*/
      content: "";
      position: absolute;
      width: 100%;
      left: 0;
      top: 0em;
    }

          /* allows y axis vals to be centered at grids plus white back */
    .barchart-YText {
      position: absolute;
      top: -8px;
      z-index: 1;  /*text on y axis is on top of the grid line, otherwise line cuts thru number text*/
      background: white;
      padding-right: 5px;
      color: #4d4d4d;
      font-size: 15px;
      font-family: 'Avenir Medium';
    }

</style>
</head>
<body>
       <!--construct html for stacked bar chart then below run 2 methods in javascript to collect json data and populate chart-->
 <div class="barchart-Wrapper">

     <div class="barchart-YCol"> <!-- this is y axis bar, we are populating y axis-->

      <div class="barchart-Y">
        <span class="barchart-YText">$1000</span>
      </div>

      <div class="barchart-Y">
        <span class="barchart-YText">$ 750</span>
      </div>

      <div class="barchart-Y">
        <span class="barchart-YText">$ 500</span>
      </div>

      <div class="barchart-Y">
        <span class="barchart-YText">$ 250</span>
      </div>

     </div> <!--end YCol-->

     <div class="barChart-Container">  <!--need container so width is applied to y axis-->
<div class="barchart"> <!--this is the class with grid lines across-->
           <!-- below dropped chart class inside barchart class which is in separate container-->
     <div class="chart" id="navContainer" style='width: 500px; height: 500px;'> <!--this is the chart container-->

<script> <!--this script has 2 methods-->
var product_1_total = []; //need to declare these as global for final use in bar chart
var product_2_total = []; //is an array for purpose of feeding into sections of one bar for one month, i is 5 months here
var product_3_total = [];
var percent_of_bar_height = []; //bar height as percent of total y axis
var percent_of_section_height_1 = []; //percent of total section height per section of each bar for product 1
var percent_of_section_height_2 = []; //percent of total section height per section of each bar for product 2
var percent_of_section_height_3 = []; //percent of total section height per section of each bar for product 3

  //first we transform our json data, then feed results to bar chart in the applyData method
transform_original_array(); //run this method first then at end of first method run the applyData method

        //applyData(); //dont run applyData here, run at end of transform method
 function applyData() {
    /*      //only use this part for live data otherwise json source is below, my live data is stored in aws s3 bucket
      const response = await fetch('https://xxxxx.s3.us-east-2.amazonaws.com/orders2.json');
      const data = await response.json();
      console.log(data);

      var length = data.length;
     */
let dates = ["jan", "feb", "mar", "apr", "may"];  //x axis labels

var div = document.getElementById('navContainer'); //to be passed into new div
   // create a new div (with id and classes 'new div'): must be done to work in script below using i variable
var new_div = document.createElement('div'); //pass in div var so our new div behaves like old div
                   new_div.style.width = "500px";
                   new_div.style.height = "500px"; //needs height attribute
                   new_div.setAttribute("id", "navContainer");  //adding these id and class atts here displays bars from bottom and not upside down or backwards
                   new_div.setAttribute("class", "chart");       //adding, below fixed here plus above line, also done so bars display vertically and not horizontal

//for(var i = 0; i< dates.length; i++) {  //use this line with live data
for(var i = 0; i < 5; i++) {              //use this line when importing local json data
                              // i represents jan, feb, mar, apr, may
                         //we are constructing 1 bar here to be used 5 times using above constructed new_div
 new_div.innerHTML +="<div class='bar' style='--bar-height: "+percent_of_bar_height[i]+"%"+";'>"+ //setting bar height as percent of tot dollars on y axis
            //next 3 lines represent 1 bar with 3 sections, each bar has 3 data vals, 3 section heights and 1 bar height 
"<div class='section' style='--section-value: "+percent_of_section_height_3[i]+";' data-value='"+product_3_total[i]+"'></div>"+
"<div class='section' style='--section-value: "+percent_of_section_height_2[i]+";'data-value='"+product_2_total[i]+"'></div>"+
"<div class='section' style='--section-value: "+percent_of_section_height_1[i]+";'data-value='"+product_1_total[i]+"'></div>"+            //setting section percentage of product1 total dollars

"<div class='label'>"+dates[i]+"</div></div>";  //add month names to x axis

      // now append the element (as a whole) to wrapper
  div.appendChild(new_div); //appends newly created div to original div.. needed for script to work
}  //end for loop

} //end apply data function    

     //below is starting function

 function transform_original_array() {

       //note that product_line and dollar_amt are not used until we find a grouping solution
var original_array =
[{"order_date":"01-03-22","product_line":"prod1","dollar_amt":100,"product1":200,"product2":0,"product3":0},
{"order_date":"01-02-22","product_line":"prod2","dollar_amt":50,"product1":0,"product2":50,"product3":0},
{"order_date":"1-16-22","product_line":"prod3","dollar_amt":50,"product1":0,"product2":200,"product3":50},
{"order_date":"1-17-22","product_line":"prod1","dollar_amt":100,"product1":100,"product2":0,"product3":0},
{"order_date":"1-15-22","product_line":"prod2","dollar_amt":50,"product1":0,"product2":50,"product3":0},
{"order_date":"2-5-22","product_line":"prod1","dollar_amt":100,"product1":100,"product2":0,"product3":0},
{"order_date":"2-6-22","product_line":"prod3","dollar_amt":20,"product1":0,"product2":30,"product3":20},
{"order_date":"2-7-22","product_line":"prod1","dollar_amt":100,"product1":100,"product2":0,"product3":0},
{"order_date":"3-23-22","product_line":"prod2","dollar_amt":200,"product1":0,"product2":200,"product3":0},
{"order_date":"3-5-22","product_line":"prod3","dollar_amt":20,"product1":0,"product2":100,"product3":20},
{"order_date":"3-29-22","product_line":"prod1","dollar_amt":100,"product1":100,"product2":0,"product3":0},
{"order_date":"3-25-22","product_line":"prod1","dollar_amt":100,"product1":100,"product2":0,"product3":0},
{"order_date":"4-23-22","product_line":"prod1","dollar_amt":500,"product1":500,"product2":50,"product3":50},
{"order_date":"4-24-22","product_line":"prod2","dollar_amt":100,"product1":0,"product2":100,"product3":50},
{"order_date":"5-10-22","product_line":"prod3","dollar_amt":50,"product1":0,"product2":0,"product3":50},
{"order_date":"5-15-22","product_line":"prod1","dollar_amt":500,"product1":100,"product2":0,"product3":0},
{"order_date":"5-25-22","product_line":"prod2","dollar_amt":50,"product1":0,"product2":50,"product3":150}]

; //end of array

       //first transform original array into product sums for each month array, then used to populate bar chart
const groupTimelineData = (elements, type) => {
        //for all elements in original array reduce them down to a sum for each month
      //first we need to reduce the current values so we can use them in a resultant array to populate our monthly bar chart
               // since our bar chart is by the month we need our original data transformed to 'by the month' format 

  result = elements.reduce((original_array, value) => {    //reduce above array to sums of each product line

monthNumber = moment(value.order_date, 'MM/DD/YYYY')[type]() + (type === 'month' ? 1 : 0); //declaring and formatting monthNumber where moment/type =month, starting at month 1 for index 0, we use moment.js for this

  res = original_array.find(e => e[type] === monthNumber);  //declaring interim result object and finding all timestamps in orig array where month is 1,2,3 etc

  if (!res) { //since array starts as empty initialize the array, in next line, first add then push
    res = { [type]: monthNumber, product1: 0, product2: 0, product3: 0 }; //add elements
            original_array.push(res); //then push vals into new array we are constructing
         } //end if

    res.product1 += value.product1;  //then for each month accumulate our sum for each subsequent pass
    res.product2 += value.product2;  
    res.product3 += value.product3;

    return original_array;

  }, []);  // The initial value is an empty object

  return result; // result is the final interim array
};  //end of groupTimelineData

      console.log(groupTimelineData(original_array, 'month'));  //this is the result object, 3 product sums for each month, original array is now transformed

console.log(JSON.stringify(result, null, 1)); //returns new resultant array in readable json format


     //part 2
//now that result is name of new array we can operate on it and do some math
           //iterating over resultant array in new json format

    for (let getProductTotal of result) {
      let total1 = getProductTotal.product1;
      let total2 = getProductTotal.product2; 
      let total3 = getProductTotal.product3;
let total =total1+total2+total3; //need new var for total
let z1 = (total/1000)*100; //new var for percent

let z2 = (total1/total)*100;  //percent of section prod1
let z3 = (total2/total)*100;  //percent of section prod2
let z4 = (total3/total)*100;  //percent of section prod3
       percent_of_section_height_1.push(z2);
       percent_of_section_height_2.push(z3);
       percent_of_section_height_3.push(z4);
//console.log("sub2 "+z2); //running incremented percent_of_bar_height array
//console.log(percent_of_section_height_1);  //section percent array, running increments
//console.log("sec3 "+percent_of_section_height_3);  //section percent array, running increments
          //push all calcs into arrays in order to populate bar chart for 5 passes (jan,feb,mar,apr,may)
       product_1_total.push(total1); //pushing total1 into bar chart, need to do same for others
       product_2_total.push(total2);
       product_3_total.push(total3);
       percent_of_bar_height.push(z1);
//console.log ("total = "+total); //total for first pass etc
//console.log ("pcct = "+z1);
         } //end for loop

console.log(percent_of_bar_height); //total bar as percent of total dollars in y axis

//object of above method is to produce array for each product key so ultimate for loop can use the array vals for each month

    applyData(); //now run this method found above to populate bar chart

} //end of transform_original_array method


</script>

</div> <!-- closing tag of chart class-->
     </div> <!--end barchart div-->
   </div> <!--end bar chart container-->
    </div> <!--end bar chart wrapper-->

       <!--chart keys-->
<b>dollar sales by product line per month</b>
 <p>magenta = product 1</p>
<p>blue = product 2</p>
<p>green = product 3</p>

</body>

</html>

Enter fullscreen mode Exit fullscreen mode

Thanks for reading. For more about Rick Delpo click https://howtolearnjava.com or click https://javasqlweb.org

Top comments (0)