Creating bar charts

@PaulOB ,

There is a bit of a time-difference/time-delay here. :wink:

Last night, on my own, I started playing around with a new, horizontal-only, bar chart and got it pretty far along, including using absolute positioning to put the number values to the right of the bars.

After supper, I will compare what you presented with my code.

Hopefully my solution on this new bar chart can be applied to the first one, and hopefully we came to similar solutions.


Looks good!


Nice effect!

Okay.


Maybe, but it just feels a little hokey since it uses concepts of ā€œbackground-imageā€ and ā€œgradientsā€ and the ā€œbackgroundā€.

You’d think there i a command that says, "Draw a repeating horizontal (or vertical) line that is // and repeat it ever ____ pixels.

(Was sure that i saw such a CSS function somewhere in the past…)

Agreed. Plus if things expanded you’d have to manually change things.

@PaulOB ,

It looks like we took a similar approach and arrived at the same results on this.

Thanks for the screenshots for reference - things are looking really good thanks to you! :+1: :+1:


It seems like there is only one major thing missing…

I wanted to circle back on the last question I asked which was…


In the real-world, most bar-charts you will see have labels and scales on both the X-axis and the Y-axis. (A bar chart is, after all, a two-dimensional chart!)

In the example above, we have the categories for the X-axis (on desktop) and the Y-axis (on mobile), but there really should be a scale with labels on the opposing axes, respectively.

The reader is looking at a bar chart reporting months of the year, and each bar represents a percentage, but a percentage of what??

(And you can’t always count on the chart title to clue you in on what the non-category scale is reporting.)


Maybe this last ask is too complicated for using HTML tables and CSS, but i figured that I would ask, because otherwise I think your responsive bar-chart looks awesome!! :slight_smile:


Ideally, the (desktop) Y-axis scale/labels could come from the HTML table, so that when I program this in PHP, I can keep things dynamic.

More real-life scenarios might include…

  • Sales by Month

  • Votes on Favorite Vegetables

  • Customers Served (in a Restaurant) by Hour
    and so on…


The axis label is obviously important, but having a labeled scale also helps the chart not get too cluttered if you have more categories, and some the values (e.g. $20,000,000) are really large.


But in a worst-case scenario, at least adding a label on the scale would better clue in the reader as to what we are reporting.

Curious to see if you have any suggestions.

Thanks as always!

As I said at the start the need is to keep things simple and if you want all bells and whistles then there are libraries and plugins that will do that for you.

You can add extra table cells/rows at the side and the bottom and set some extra legends to aid legibility but then you could also add a separate key underneath the graph to define any areas than may need explanation (like most graphs you see). I see a graph as a quick visualisation of the data but for detailed analysis you probably want the table data itself in more complicated scenarios.

I’ve added a rotated legend to the side with numbers down the edge and an extra legend underneath the months. This is just proof of concept and you would need to ensure data will fit in the spaces allocated or cater for them by other means. The rotated legend should not wrap as it will spoil things and indeed be too hard to manage as the rotation takes it out of the flow.

Barring minor tweaks I think this is as good as it gets without resorting to multiple graphics and svg or library plugins.

2 Likes

@PaulOB ,

Hi there. Sorry for the delay. Have spent the last two days looking at your latest code.

After tweaking things to my liking, I would say that this responsive bar chart is about 95% done…

The only nagging problem I have with my latest code is that the spacing of the Y-axis marker numbers is dodgy.

Maybe some context will help my concern…

My end goal is to create a PHP function - using this HTML/CSS code - where I can grab data from my database, feed it to the PHP function, and have it generate a really cool - and responsive - bar chart.

In order to make it worth the effort, my bar chart needs to be 100% responsive to dynamic data.

For one bar chart, the Y-axis might be a ā€œcountā€ going from 1 to 10 (e.g. ā€œBicycles Ownedā€) with increments of ā€œ1ā€.

For another bar chart, the Y-axis might be ā€œtotalā€ going from $1 to $125 (e.g. ā€œWeekly Meal Costsā€), with increments of $25.

And for another bar chart, the Y-axis might be ā€œtotalā€ going from $1 to $1,800 (e.g. ā€œMonthly Salesā€) with increments of $100.

The point being, is that I really need a way to ā€œsynch upā€/ā€œanchorā€ the Y-axis numbers with the gridlines, because if the numbers ā€œdriftā€ from the gridlines, then the Y-axis numbers are useless. :frowning:

(I’m thinking that if I can create different CSS styles for splitting the chart up into either 4, 5, 6, 7, 8, 9, 10 increments, then that would cover all future scenarios. Since I can use a percentage on the gridlines, I’m okay there, but it is aligning the numbers up to different scenarios that has me worried, if that makes sense?!)


Below is my latest code - which incorporates your really neat Y-axis labels/number-scale - but is cleaned up some and which has the final look that I am after…

<!DOCTYPE HTML>
<html lang="en">

<!-- *************************  HTML HEAD  ********************************* -->
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1">

  <title>sp_bar-chart_v07.html</title>

  <!-- CSS STYLES -->
  <style media="screen">
    /**************************************************************************/
    /* GENERIC                                                                */
    /**************************************************************************/
    * {
      margin: 0;
      padding: 0;
    }
    
    *,
    *:before,
    *:after {
      box-sizing: border-box;
    }

    body{
      font-family: Helvetica, Arial, Sans-Serif;
      font-weight: normal;
      line-height: 1.4em;
      font-size: 0.9em;
      color: #000;
    }
    
    .chart .jan span{
      background: green;
    }
    
    .chart .feb span{
      background: orange;
    }
    
    .chart .mar span{
      background: teal;
    }
    
    .chart .apr span{
      background: cyan;
    }
    
    .chart .may span{
      background: lightblue;
    }
    
    .chart .jun span{
      background: red;
    }
    
    .chart .jul span{
      background: magenta;
    }
    
    .chart .aug span{
      background: yellow;
    }
    
    .chart .sep span{
      background: skyblue;
    }
    
    .chart .oct span{
      background: gray;
    }
    
    .chart .nov span{
      background: aquamarine;
    }
    
    .chart .dec span{
      background: white;
    }

    
    /**************************************************************************/
    /* DESKTOP (Horizontal)                                                   */
    /**************************************************************************/
    
    /* TABLE */
    .chart{
      table-layout: fixed;
      width: 90%;
      max-width: 1240px;      
      height: 60vh;
      border-collapse: collapse;
      margin: auto;
    }

    /* CAPTION */
    caption{
      padding: 20px 0 10px 0;
      font-size: 1rem;
      font-weight: bold;
      text-align: center;
      line-height: 1.4;
    }
    
    caption > small{
      display: block;
      font-size: 0.8rem;
      font-weight: normal;
    }

    /* THEAD */
    .chart thead th{
      border-right: 2px solid #000;
    }
    
    .chart thead td{
      height: 20px;
      background: #F9F9F9;
    }

    /* TFOOT */
    .chart tfoot th{
      padding: 5px 0 0 0;
      vertical-align: top;
      font-size: 0.9rem;
      font-weight: normal;
      background-color: #FFF;
    }
    
    .chart tfoot td.x-axis{
      padding: 0.4rem 0 0 0;
      font-size: 0.9rem;
      font-weight: bold;
      text-align: center;
    }

    /* TBODY */
    .chart tbody{
      background: #F9F9F9;
      background-image: linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px);
      background-size: 98% 10%;
    }
    
    /* TD */
    .chart tbody td{
      vertical-align: bottom;
      height: 100%;
      padding: 0 10px;
      border-bottom: 2px solid #000;
    }
    
    /* BARS */
    .chart tbody td span{
      position: relative;
      display: block;
      background: #99FFFF;
      border-top: 1px solid #000;
      border-right: 1px solid #000;
      border-bottom: none;
      border-left: 1px solid #000;
      box-shadow: 5px 0px 5px rgba(0, 0, 0, 0.3);
    }

    /* BAR-VALUES */
    .chart tbody td span b{
      position: absolute;
      display: block;
      bottom: 100%;
      left: 0;
      right: 0;
      text-align: center;
      font-weight: normal;
    }
    
    

    /* Y-AXIS LABEL + MARKERS */
    .chart tbody th.y-axis{
      position: relative;
      border-right: 2px solid #000;
      background: #FFF;
    }
    
    
    /* Transform Y-axis Label */
    .rotate {
      position: relative;
      -ms-writing-mode: tb-lr;
      z-index: 3;
      margin: 1rem 1rem 0 0;
      font-size: 1rem;
      font-weight: bold;
    }

    @supports (writing-mode: vertical-lr) {
      .rotate {
        display: inline-block;
        writing-mode: vertical-lr;
        white-space: nowrap;
        transform: rotate(180deg) translateX(20px);
        line-height: 0;
      }
    }
    
    .rotate > small{
      font-size: 0.8rem;
      font-weight: normal;
    }

    /* Y-axis Markers */
    ol.segments{
      position: absolute;
      top: -27px;                           /* LOOK */
      bottom: 0;
      right: 3px;
      margin: 0;
      padding: 0;
      list-style: none;
      font-size: 0.9rem;
      font-weight: normal;
      display: flex;
      flex-direction: column;
      transform: translateY(6%);            /* LOOK */
    }
    
    ol.segments li{
      flex: 1 0 0;
    }

    
    /**************************************************************************/
    /* MOBILE (Vertical)                                                      */
    /**************************************************************************/
    @media screen and (max-width:601px){
      
      /* TABLE */
      .mobile-optimised{
        display: block;
        height: auto;
        width: 100%;
        overflow-wrap: break-word;
        border-spacing: 0;
      }

      /* CAPTION */
      .mobile-optimised caption{
        display: block;
        padding: 1rem 0;
      }

      /* THEAD + TFOOT */
      .mobile-optimised thead,
      .mobile-optimised tfoot{
        display: none
      }

      /* TBODY */
      .mobile-optimised tbody,
      .mobile-optimised tr{
        display: block
      }
      
      .mobile-optimised tbody:after,
      .mobile-optimised tr:after{
        content: "";
        display: block;
        clear: both;
        height: 0;
      }
      
      .mobile-optimised tbody{
        margin: 0 4rem 2rem 2rem;
        border-left: none;
        background: #F9F9F9;
      }
      
      /* TR */
      .mobile-optimised tbody tr{
        background-image: linear-gradient(to right, #DDD 1px, transparent 1px);
        background-size: 10% 100%;
        border-right: 1px solid #DDD;
      }
      
      /* TD */
      .mobile-optimised tbody td:before{
        content: attr(data-th);
        display: block;
        font-weight: bold;
        margin: 0 0 0 5px;
      }
      
      .mobile-optimised tbody td{
        display: block;
        float: left;
        height: auto;
        width: 100%;
        clear: both;
        margin: 0;
        padding: 1rem 0 0 0;
        white-space: nowrap;
        border-bottom: none;
        border-left: 2px solid #000;
        background: transparent;
      }
      
      .mobile-optimised tbody td:nth-child(2){
        padding: 1.5rem 0 0 0;
      }
      
      .mobile-optimised tbody td:last-child{
        padding: 1rem 0 1.5rem 0;
      }

      /* BARS */
      .mobile-optimised tbody td span{
        position: relative;
        height: 25px!important;
        line-height: 25px;
        border-top: 1px solid #000;
        border-right: 1px solid #000;
        border-bottom: 1px solid #000;
        border-left: none;
        box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3);
      }

      /* BAR-VALUES */
      .mobile-optimised tbody td span b{
        position: absolute;                 
        left: 100%;
        top: 0;
        right: auto;
        bottom: 0;
        padding: 0 0 0 5px;
      }



      /* Y-AXIS LABEL + MARKERS */
      .mobile-optimised tbody th.y-axis{
        display: block;
        width: calc(100% + 2px);            /* hide right border */
        padding-bottom: 1.4rem;
        border-right: none;
        border-bottom: 2px solid #000;
        border-left: none;
        background-color: #FFF;
      }
      
      /* Y-axis Label */
      .mobile-optimised tbody .rotate{
        display: block;
        -ms-writing-mode: initial;
        writing-mode: initial;
        white-space: nowrap;
        transform: none;
        margin: 0;
        line-height: normal;
        font-size: 0.9rem;
      }

      /* Y-axis Markers */
      .mobile-optimised tbody ol.segments{
        position: absolute;
        top: auto;
        bottom: 0;
        right: 0;
        left: -37px;                        /* LOOK */
        font-size: 0.9rem;
        flex-direction: row-reverse;
        transform: translateX(1rem);        /* LOOK */
      }
      
      .mobile-optimised tbody ol.segments li{
        flex: 1 0 0;
        text-align: right;
      }
      
    }/* End of MOBILE Styles. */

    
    /* ANIMATION */
    .chart span{
      opacity: 1;
      animation: barchart 2s ease reverse;
    }
    
    @keyframes barchart{  
      to{
        height: 0%;
        opacity: 0;
      }  
    }

  </style>
</head>

<!-- *************************  HTML BODY  ********************************* -->
<body>
  <table class="chart mobile-optimised">
    <caption>
      Total Monthly Widget Sales
      <small>(1/1 - 12/31/2020)</small>
    </caption>

    <!-- Used to establish equal column-widths. -->
    <thead>
      <tr>
        <th></th>
        <td colspan="12"></td>
      </tr>
    </thead>

    <tbody>
      <!-- inline styles used so values can be entered more easily from the backend (php etc). e.g. width and height match the percentage data value. -->
      <!-- Note the use of the data-th attributes which are used to display headings for the mobile version. Therefore the Normal heading text must be duplicated in the data-th values as shown below. -->
      <tr>
        <th class="y-axis">
          <div class="rotate">Revenue <small>(US Dollars)</small></div>
          <ol class="segments">
            <li>$100k</li>
            <li></li>
            <li>$80k</li>
            <li></li>
            <li>$60k</li>
            <li></li>
            <li>$40k</li>
            <li></li>
            <li>$20k</li>
            <li></li>
            <li>$0k</li>
          </ol>
        </th>
        
        <!-- Classes below serve as styling "hooks"... -->
        <td style="width:70%" data-th="January" class="jan"><span style="height:70%"><b>$70k</b></span></td>
        <td style="width:10%" data-th="February" class="feb"><span style="height:10%"><b>$10k</b></span></td>
        <td style="width:20%" data-th="March" class="mar"><span style="height:20%"><b>$20k</b></span></td>
        <td style="width:40%" data-th="April" class="apr"><span style="height:40%"><b>$40k</b></span></td>
        <td style="width:100%" data-th="May" class="may"><span style="height:100%"><b>$100k</b></span></td>
        <td style="width:15%" data-th="June" class="jun"><span style="height:15%"><b>$15k</b></span></td>
        <td style="width:60%" data-th="July" class="jul"><span style="height:60%"><b>$60k</b></span></td>
        <td style="width:55%" data-th="August" class="aug"><span style="height:55%"><b>$55k</b></span></td>
        <td style="width:35%" data-th="September" class="sep"><span style="height:35%"><b>$35k</b></span></td>
        <td style="width:90%" data-th="October" class="oct"><span style="height:90%"><b>$90k</b></span></td>
        <td style="width:20%" data-th="November" class="nov"><span style="height:20%"><b>$20k</b></span></td>
        <td style="width:50%" data-th="December" class="dec"><span style="height:50%"><b>$50k</b></span></td>
      </tr>
    </tbody>

    <tfoot>
      <tr>
        <th class="empty"></th>
        <th scope="col">Jan</th>
        <th scope="col">Feb</th>
        <th scope="col">Mar</th>
        <th scope="col">Apr</th>
        <th scope="col">May</th>
        <th scope="col">Jun</th>
        <th scope="col">Jul</th>
        <th scope="col">Aug</th>
        <th scope="col">Sep</th>
        <th scope="col">Oct</th>
        <th scope="col">Nov</th>
        <th scope="col">Dec</th>
      </tr>
      <tr>
        <td class="emptyfoot"></td>
        <td class="x-axis" colspan="12">Month</td>
      </tr>
    </tfoot>
  </table>
  
</body>
</html>

(I added ā€œLOOKā€ comments in my code to draw your attention to where I need help.)


What I have now looks pretty good, but if you change device size, OR we change the number of gridlines, i think my code will fall apart pretty quick…


This is frustrating, because I feel like this is so close to being a near-perfect solution, and am hoping that you being a CSS guru, that you can offer some help figuring out how to make this work in any, and all, situations?!


P.S. I like how you changed to using percentages (versus pixels) for the gradient as below…

background-size: 10% 100%;

Using percentages all but guarantees that the gridlines will adapt to however many increments I need, as well as to any device.

If I could apply a similar technique to the Y-axis numbers then they would always synch up/anchor to the gridlines and I would be golden!!

Hope all of this makes sense?

Thanks!

:slight_smile:

Yes you can use 10% as percentage for the horizontal lines and still get the same results as the vh unit I used. I used vh units because the element itself had a vh unit height and it just made sense to divide by vh. However the 10% would mean you don’t need to change it if you changed the vh height of the graph so would be a better option.

In your example you made a few little mistakes.

Firstly you divided the 10 graph lines with eleven elements (you added an extra zero value and 5 empty spaces). That means you can never divide that space evenly to match the 10 graph lines. You must have the same number of elements (or double or half) to have an even space to work width.

You then compounded the issue by increasing the top position of the element by 27px (a magic number) thus making any equal division of that space impossible. Remember the flexbox items occupy the whole space from top to bottom evenly. If you increase the space or add items then you break the calculation.

Lastly you then translated the item by 6% which is another magic number and not really related to anything. In my example I used half the font-size which moved the text half the font upwards so it crossed the line. The text was always going to be just under the top line so all that was needed was to move the text up by its own half height.

Therefore I suggest that if you wan an extra zero row you absolutely position that zero at the bottom and remove it from the flexbox flow. That means the other 5 flexbox items will automatically distribute themselves easily over the whole column height exactly.

In order to move the text a little bit upwards so it crosses the graph line I suggest adding an extra inner element (a ā€˜b’ element in this new example) and then move that b element upwards by half its height. That will make the element straddle the graph line (you can remove the bold styling in the css if you want).

These changes will not therefore be magic numbers as such and will work for any number of items that you require. If you have 20 graph lines then you need to have 20 numbers on the axis (or an exactly divisible number) which is why zero cannot be part of this flow.

I copied your pen exactly and added those changes above and I see no drift on the numbers.

You made similar mistakes on the small screen. Don’t alter the top, bottom, left or right positions otherwise you no longer have a 100% height/width to work with.

Remember percentages mean different things in different contexts. In transforms they relate to the element itself but in heights or widths they refer to the containing block. You have to be sure that what you move is the element itself in relation to itself and not in relation to its containing block.

If you are consistent then the numbers will line up. You must have a full 100% height or width with the absolute axis so that the flexbox will automatically distribute the correct number of items. Then the only issue is just to move the text so it crosses the boundary. The text by default will sit inside the boundary so by moving the text by half its width or height you will always get a correct intersection (barring rounding errors etc).

Whatever you do it must make logical sense and not just because it fits this case only.

In the end you could also set up custom css variables at the start of the code to set the number of gridlines depending on how many you want. That means you’d only need to change the code in one place when you set up a different number of items.

Roughly like this:

1 Like

I recommend this it is a very cool chart

And the external resources for JS is this
https://code.jquery.com/jquery-1.11.2.min.js

Hi there Codeman,

I think that you must have overlooked this…

…in the original post to this thread. :rofl:

Don’t feel too guilty though, I am often guilty of similar oversights when
replying to threads. :wonky:

coothead

1 Like

@PaulOB ,

I have been feeling dizzy the last 2-3 days… :frowning:

That probably explains why I can’t seem to get your code working above.

Will hopefully be in touch soon…

@PaulOB ,

First off, let me thank you for all of your help on this bar chart project! (You are truly a ā€œgodā€ of HTML and CSS!!) :+1: :+1: :+1:

Am feeling a little better today, and was able to add your changes to my existing codebase - which had all of my comments - and now things match your results.

I have learned some really cool things here at SitePoint, but this responsive bar chart may be one of the coolest HTML/CSS solutions that I have ever seen?! :slight_smile:


About the only remaining issue - that I can see right now - is that there is this strange ā€œfat lineā€ at the top of my Y-axis of the desktop view as seen here…

Fat Line

For desktop, my Y-axis is actually two parts, because I wanted the Y-axis to extended beyond the 100%.

For my HTML, I have this…

<thead>
  <tr>
    <th></th>
	<td colspan="12"></td>
  </tr>
</thead>

And for the CSS…

.chart thead th{
  border-right: 2px solid #000;
}

The reason I used a <th></th> was to differentiate it from the <td colspan="12">.

This appears to be an issue with Firefox because it looks okay in Chrome.

Any idea why this is happening?


Oh, and on your last chart…

It looks like you just created 3 HTML tables and the CSS was the same as before? (I just skimmed it, since it would take forever to do a line-by-line comparison.)

That seems to be a bug in Firefox with a mis-alignment in its border collapse routine. It looks like it can be fixed by removing the border-collapse and set border-spacing to zero instead.

e.g.

  /*border-collapse: collapse;*/
  border-spacing:0;/* firefox bug with border mis-alignment*/
.chart {
  width: 100%;
  table-layout: fixed;
  /*border-collapse: collapse;*/
  border-spacing:0;/* firefox bug with border mis-alignment*/
  max-width: 1240px;
  margin: auto;
  background: #f9f9f9;
}

Historically Firefox’s borders on tables were slightly misaligned in edge cases so I guess they still haven’t fixed it completely.

The three tables all use the same css apart from the one custom variable I changed. The html was changed to show different numbers of items and rows but the CSS was using a custom property (css variable) in order to make it ease to change the axis labels. Custom properties are only available in modern browsers but make life easier for things like this.

e.g.

:root {
  --gridlines: 10; /* the number of gridlines you want */
  /* we will modify this with a class for other tables */
}


/* redefine the gridlines for the other tables using an extra class on the table */
/* bi monthly chart */
.bimonthly {
  --gridlines: 5; /* the number of gridlines you want */
}
/* regional graph*/
.regional {
  --gridlines: 4; /* the number of gridlines you want */
}
1 Like

Thanks!

Okay, I missed that. I will read up on variables for the future.

Thanks for all of your help. @PaulOB. This is the coolst bar chart that I’ve ever seen online!


P.S. @coothead thank you for posting your SVG solution early on. I didn’t mean to shoot it down, but was curious about making things accessible, and as @PaulOB has shown, it seems that using HTML tables/data and then using CSS to convert them into a pretty bar chart is the best solution. However, I will use your code above as ā€œinspirationā€ when I get into fancier graphs and charts that cannot be solved using simple HTML/CSS techniques, so your code will be a great stepping stone. In fact, after I complete a standalone horizontal bar chart, my next goal is to learn how to build an accessible and responsive pie chart, and I’m think SVG is a better way to go, but I’ll start a thread on that when the time comes.

Thanks everyone!! :+1:

1 Like

I was procrastinating and updated the following online demos (item #7) which now includes a separate dynamic chart. source code included:

https://www.johns-jokes.com/downloads/sp-h/jb-svg-tooltips/

You might like to have a look at this…

SVD-arc-creator.zip (123.8 KB)

…in the meantime. :winky:

2 Likes

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.