Skip to content

Instantly share code, notes, and snippets.

@marsbomber
Created December 15, 2012 07:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marsbomber/4291868 to your computer and use it in GitHub Desktop.
Save marsbomber/4291868 to your computer and use it in GitHub Desktop.
Make up 100% CSS width from doing percentage calculation
I'll illustrate the problem with an example.
I query the database for some aggregated results. Say there are 4
aggregated counters I'm interested in and wanting to display them
in a 100% width div. Each of the counters should contribute to a
percentage of the full width.
For instance, my db query returns me somthing like this
| counter_1 | counter_2 | counter_3 | counter 4|
| 1 | 1 | 1 | 1 |
I need to construct some HTML markup similar to this
<div class="bar">
<span class="first" width="25%">Counter 1 - 25%</span>
<span class="middle" width="25%">Counter 2 - 25%</span>
<span class="middle" width="25%">Counter 3 - 25%</span>
<span class="last" width="25%">Counter 4 - 25%</span>
</div>
The above markup is easy enough to generate, some simple calculation
will give us
total_count = 4
counter_1% = 1/4 = 25%
counter_2% = 1/4 = 25%
counter_3% = 1/4 = 25%
counter_4% = 1/4 = 25%
exactly what we are after.
Now, consider the following
| counter_1 | counter_2 | counter_3 | counter 4|
| 1 | 1 | 1 | 0 |
total_count = 3
counter_1% = 1/3 = 33.3333333333%
counter_2% = 1/3 = 33.3333333333%
counter_3% = 1/3 = 33.3333333333%
counter_4% = 0/3 = 0%
We can see the fraction number is 100% precise (of course ...) but
unfortunately, we cannot use things like 1/3 in CSS (as far as I know).
So some kind of rounding/flooring/ceiling (doesn't matter which one
you pick, it'll become problematic) needs to be done ...
Let's use rounding here (solution 1)
total_count = 3
counter_1% = 1/3 = 33%
counter_2% = 1/3 = 33%
counter_3% = 1/3 = 33%
counter_4% = 0/3 = 0%
This will produce
<div class="bar">
<span class="first" width="33%">Counter 1 - 33%</span>
<span class="middle" width="33%">Counter 2 - 33%</span>
<span class="middle" width="33%">Counter 3 - 33%</span>
<span class="last" width="0%">Counter 4 - 0%</span>
</div>
It's somewhat acceptable, if the client doesn't mind the fact that
he/she is missing 1% on the stats ...
If we use a calculation like "Round each one until the last,
then subtract the partial sum from the total to get that one",
which is the exact strategy I used to start with ... It actually makes it worse.
total_count = 3
counter_1% = 1/3 = 33% (round)
counter_2% = 1/3 = 33% (round)
counter_3% = 1/3 = 33% (round)
counter_4% = 0/3 = 100% - 33% * 3 (remainder) = 1%
You can see, this just becomes simply wrong...
So ... At the end, I use solution 1, but rounded with 2 more 2 decimals
total_count = 3
counter_1% = 1/3 = 33.33%
counter_2% = 1/3 = 33.33%
counter_3% = 1/3 = 33.33%
counter_4% = 0/3 = 0%
<div class="bar">
<span class="first" width="33.33%">Counter 1 - 33.33%</span>
<span class="middle" width="33.33%">Counter 2 - 33.33%</span>
<span class="middle" width="33.33%">Counter 3 - 33.33%</span>
<span class="last" width="0%">Counter 4 - 0%</span>
</div>
Still not a full 100%, but I hope it's more acceptable...
@cjheath
Copy link

cjheath commented Dec 15, 2012

Take the first one, round(100_(1/3)) or 33%, leaving 67%. You have 1/2 left, that's round(67_(1/2)), which rounds upwards to 34%. The last one must be 67-34 = 33. This sort of progressive rounding then dividing the remainder is the best you can do without going to fractional percentages (and even then the same principle applies, but rounding to the nearest 0.1 or 0.01.

@cjheath
Copy link

cjheath commented Dec 15, 2012

... so to refine your "at the end" solution, you decide on 33.33% for the first one, that leaves 66.67% Divide that in half, you get 33.34%, leaving 33.33% over. The point is, round each one, subtract it from the total, then divide the remainder. This effectively is how Bresenhams algorithm work for drawing pixels to approximate a vector. It's well-established as the best available approximation.

@marsbomber
Copy link
Author

I see what you mean. Thanks man!

I quickly hacked out something. It should do what you suggested :)

stats = [1, 1, 1, 0]

def percentagerize(collection, result=[], remainder=1)
  return result if collection.count == 0

  denominator = collection.inject(:+)
  dup_collection = collection.dup

  numerator = dup_collection.shift

  if denominator == 0
    percent = 0.0
  else
    percent = (remainder * (numerator / denominator.to_f)).round(4)
  end

  remainder -= percent
  result << percent

  percentagerize(dup_collection, result, remainder)
end

puts percentagerize(stats)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment