Reputation: 9525
I need to make a simple bar progress indicator - actually I just need a bar with a green portion for the good amount and another coloured portion for the bad amount and the percent good somewhere in the middle of the bar. Assume the bar is 50px wide and 15px high. Think static progress bar like example below.
I have done this before using jquery and a background div with 2 further divs positioned on top of it for the good and bad indicators, then a positioned div above that to display the text.
However I am wondering if there is a much more simple HTML5 canvas, SVG or CSS solution. As this timy control will appear in every row of a long table, the aim is to pollute the DOM less and make greater readability and re-usability.
I know there are libraries that do this but I wanted to use it as a learning experience. The solution should be either no script, or JS only, or JS with jquery.
EDIT: Thanks for the positive input folks. I propose my own solution with working snippet in the answers below as I think it is worthy of standing alone for votes etc. No-one has proposed an SVG solution yet - any takers?
Image of my propsal:
EDIT 2: 3 excellent solutions (plus mine) so far. Has turned out to be quite an interesting weekend challenge. Any more ?
EDIT - ANSWER SELECTED: In the interests of closure, I have selected and answer to award the points. However, all of the proposed answers seem viable in different situations. For my purposes the SVG-based answer from Mr Le Beau was the optimum. My parameters for selection were that I initially make the page markup on the server and can therefore set all the values necessary to render the bars without code execution. Later I allow a change to the percent complete which I accomplish with an ajax post to the server and simple jquery to update the text and bar coverage.
I would hope that in time the HTML5 progress tag answer will make this question redundant, but then again we might all be sitting on the deck at the developers rest home sipping cocktails by then (maybe).
Thank you all for your efforts.
Upvotes: 2
Views: 2361
Reputation: 101810
Here's an SVG equivalent of your offering.
$("svg.tbc").each(function(i, item) {
var $item = $(item);
var rate = $item.parent().find(".country").attr("rate");
$item.find(".bar").attr("width", rate);
$item.find("text").text(rate);
});
.tbc {
width: 50px;
height: 15px;
}
.tbc .bg {
fill: gold;
fill-opacity: 0.5;
}
.tbc .bar {
fill: blue;
fill-opacity: 0.5;
}
.tbc text {
font-size: 8pt;
font-family: Calibri, sans-serif;
font-weight: bold;
fill: blue;
}
.country {
display: inline-block;
width: 200px;
}
.info {
margin-top: 20;
font-size: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h1>Urban population rate by country </h1>
<div>
<div class="country" rate="57.6%">China</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div>
<div class="country" rate="32%">India</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div>
<div class="country" rate="82.1%">U.S.</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div>
<div class="country" rate="73.2%">Russia</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div>
<div class="country" rate="81.2%">UK</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div>
<div class="country" rate="11.5%">Burundi</div>
<svg class="tbc"><rect class="bg" width="100%" height="100%"/><rect class="bar" width="0%" height="100%"/><text x="50%" y="70%" text-anchor="middle">0%</text></svg>
</div>
<div class="info"><a href="http://www.worldometers.info/world-population/population-by-country/" target="">Source: www.worldometers.info/</a>
Upvotes: 3
Reputation: 9525
Here is my own offering. Note that a large part of the script is generating a background image with transparency from a canvas which you may not need to do.
The technique uses a single div per bar, some standard CSS for styling, and a single image.
The bar background image is pre-drawn at same height and twice the width of the bar, with two segments 50% of the width using the colours that are required to represent progress. So if the bar is 50px the background image is 100px with 50 px blue and 50px yellow, for example.
In my example I use script to read the value to be displayed out of the div, then modify the css background-position-x value to move the image appropriately. Move it to x=0 and the bar is all good, move to x= -25 and you have 50/50 good/bad, etc.
If you know the bar size and can make the image in advance, and when generating the page from a DB then you can skip the script entirely and just set the necessary element css on the divs as you output the html.
const barW = 50; // display width of bar element on page. Note green-red bg image width must be 2 x barW
/*
This is where we apply the image to the bars. Because I create the image from a canvas, this is called as a callback from the image create process. If your image is just a plain web image and you know dimensions you could run this function in your document.ready() event.
*/
function makeBars(img) {
// apply the bar background to each tiny bar class (tbc)
$('.tbc').each(function() {
var num = parseFloat($(this).html(), 10) / 100; // I stored the % in the element html - read that.
var xPos = Math.round(-(barW * (1 - num))); // offset x by minus the bad proportion.
$(this).css({
backgroundImage: 'url(' + img.src + ')',
backgroundPositionX: xPos
}); // apply to element.
})
}
/*
Everything from here is optional - it generates an image via a canvas
*/
makeImg(makeBars); // call for the background image to be made
// this is all to generate an image - made from a canvas.
function makeImg(cbf) {
// add a stage
var s = new Konva.Stage({
container: 'container',
width: 800,
height: 200
});
// add a layer
var l = new Konva.Layer();
s.add(l);
// Add a good rect to the LAYER just to show the good amount.
var green = new Konva.Rect({
fill: 'blue',
width: 50,
height: 15,
x: 0,
y: 0,
opacity: 0.5
});
l.add(green);
// Add a bad rect to the LAYER just to show the good amount.
var red = new Konva.Rect({
fill: 'gold',
width: 50,
height: 15,
x: 50,
y: 0,
opacity: 0.5
});
l.add(red);
var bg;
var catchImg = function(img) {
cbf(img); // callback passing the new img
}
l.draw();
s.toImage({
callback: catchImg,
x: 0,
y: 0,
width: 100,
height: 15
})
$('#container').remove(); // now we have an image - trash the canvas.
}
.tbc {
display: inline-block;
width: 50px;
height: 15px;
line-height: 15px;
border: 1px solid #ccc;
text-align: center;
font-size: 8pt;
font-family: Calibri, sans-serif;
font-weight: bold;
color: blue;
}
.country {
display: inline-block;
width: 200px;
}
.info {
margin-top: 20;
font-size: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.3/konva.min.js"></script>
<h1>Urban population rate by country </h1>
<div>
<div class='country'>China</div>
<div class='tbc'>57.6%</div>
</div>
<div>
<div class='country'>India</div>
<div class='tbc'>32%</div>
</div>
<div>
<div class='country'>U.S.</div>
<div class='tbc'>82.1%</div>
</div>
<div>
<div class='country'>Russia</div>
<div class='tbc'>73.2%</div>
</div>
<div>
<div class='country'>UK</div>
<div class='tbc'>81.2%</div>
</div>
<div>
<div class='country'>Burundi</div>
<div class='tbc'>11.5%</div>
</div>
<div class='info'><a href='http://www.worldometers.info/world-population/population-by-country/' target=''>Source: www.worldometers.info/</a>
<div id='container'>Temp: Used to contain canvas</div>
Upvotes: 0
Reputation: 11597
EDIT: added static solution
The absolute minimum I can come up with is this:
// p must be an INTEGER from 0 to 100
function bar(n, p)
{
document.getElementById("bar-" + n)
.firstChild.style.width = (p * 2) + "px";
}
var bars = [ 0, 0 ];
setInterval(function () {
var i;
for (i = 0; i < bars.length; i++)
{
bar(i, bars[i]);
if (bars[i] < 100)
{
bars[i] += i + 1;
}
}
}, 100);
div.bar
{
width: 200px;
height: 15px;
margin: 4px;
background-color: #ffffc0;
border: 1px solid #000000;
border-radius: 3px;
}
div.bar div
{
height: 15px;
background-color: #408040;
}
<div id="bar-0" class="bar"><div></div></div>
<div id="bar-1" class="bar"><div></div></div>
And here the static solution:
(function () {
$("[data-bar]").each(function () {
var p;
p = parseInt($(this).data("bar"), 10);
$(this)
.append($("<div>").addClass("p").text(p + "%"))
.append($("<div>").addClass("q").css("width", (2 * p) + "px"));
});
}());
div.bar
{
position: relative;
width: 200px;
height: 15px;
margin: 4px;
background-color: #ffffc0;
border: 1px solid #000000;
border-radius: 3px;
text-align: center;
}
div.bar div.p
{
position: absolute;
width: 100%;
margin-top: 1px;
font-size: 11px;
font-weight: bold;
font-family: monospace;
}
div.bar div.q
{
height: 15px;
background-color: #408040;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="bar" data-bar="32"></div>
<div class="bar" data-bar="78"></div>
Upvotes: 2
Reputation: 138257
You could use the progress element:
<progress max="100" value="0" />
Its difficult to style, but easy to use:
var i=0;
setInterval(function(){
document.getElementsByTagName("progress")[0].value=++i;
},100);
Upvotes: 2
Reputation: 9887
You could do it with just one DIV and the pseudos :before
and :after
, one for background and one for the text, like here https://jsfiddle.net/3keey3t6/2/
.progress::before {
position: absolute;
top: 0;
left: 0;
width: 100px;
bottom: 0;
background: #eee;
content: "";
z-index: -1;
}
.progress {
position:relative;
width:75px;
height:35px;
background:green;
}
.progress:after {
position: absolute;
content: attr(data-progress) '%';
width: 100%;
line-height: 35px;
text-align: center;
color: white;
text-shadow:1px 0 0 black, 0 1px 0 black, -1px 0 0 black, 0 -1px 0 black;
}
and then
<div id="pbar" class="progress" data-progress="20"/>
and
function setProgress(p) {
var prg = document.getElementById("pbar");
prg.style.width = p+"px";
prg.setAttribute("data-progress", p);
}
setProgress(10);
Just a very simple example if the goal is to pollute DOM as less as possible ...
Upvotes: 2