One day (last Thursday evening) I was bored. [Editor's note: This was written in 2017] In such cases I (sometimes) write something experimental, following some absurd idea that came to my head. If it turns out to be interesting - I load it on GitHub, sometimes even on RubyGems. This time, seemingly, it turned out to be very interesting: a little library with a foolish name worldize gathered a hundred of "stars" on GitHub in the first days of its existence, for four days in a row it was in top 10 "repositories of the day", got into the top "repositories of the week" and in a popular newsletter Ruby News Weekly.
Though I'm a little offended (none of my Important and Significant libraries ever received such an attention), still I want to tell you how this happened.
The idea and its realization
A lot of static languages and packages have an opportunity to create in one line a map of the world, where every country would be colored depending on some value (for example, average life expectancy or the amount of pandas per capita, or average cost of the apartment).
(these maps are also called choropleth maps.)
An idea came to my mind: hey, how difficult is it to make such a thing in Ruby? I have an idea, I have free time, I have Google Almighty - and here is the result:
require 'worldize'
Worldize::Countries.new.
draw_gradient(
'#D4F6C8', # gradient from this color...
'#247209', # to this
# and some numeric value for different countries:
{'Argentina' => 1, 'Bolivia' => 2, 'Chile' => 3 .... }
)
The result:
How cool iz dat?
The process
The plan of action is clear from the start:
- find publicly available data in "name of the country – coordinates of the country polygon" format;
- learn how to draw a picture with such polygons colored differently;
- wrap all of it with a simple API
Geodata and maps
I needed publicly available data and I got it from Natural Earth, and I found converting it to the format I needed – in one excellent
repository. Convenient format is GeoJSON, which, unlike many other cartographic formats, can be read and used without any additional tools and libraries.
But that's only half of the battle. Basic education says that geo-coordinates received from such datasets are the coordinates on the sphere (putting it mildly), and, to convert them to beautiful polygons on the flat we need some projection. The Google Almighty mentioned above states that the most popular and well-known projection is a WebMercator, which is used in modern online-maps. After messing with formulas, stated in Wikipedia, for some time, through trial and error we get quite a demonstrative Ruby-code:
# longitude to the x-coordinate of the picture:
# just convert from one range to another
# max_x - width of the expected picture
x = lng.rescale(-180..180, 0..max_x)
# latest version of Ruby supports names of variables in Unicode!
include Math
π = PI
φ = -lat * π / 180 # from degrees to radians required by the formula
# latitude to the y-coordinate of the picture:
# a formula with logarithms and tangents gives the value from −π to π
# all that's left to do is convert it from this range to the range "0 - height of the picture"
y = log(tan(π / 4 + φ / 2)).rescale(-π..π, 0..max_y)
A useful method rescale
, which converts a number from one range to another is defined in our library, moreover, we use a feature from Ruby 2 - refine:
module Refinements
refine Range do
def distance
self.end - self.begin
end
end
refine Numeric do
def rescale(from, to)
(self - from.begin).to_f / from.distance * to.distance + to.begin
end
end
end
# and now, in any class or module...
module TryMe
# ...where we write using Refinements...
using Refinements
# ...all the numeric variables will have the method rescale
p 8.rescale(0..10, 0..100) # => 80
end
# and in other places it won't interfere in someone else's code
p 8.rescale(0..10, 0..100) # undefined method `rescale' for 8:Fixnum (NoMethodError)
By the way, it is used not only here but also in the color gradient calculation (see below).
About the license agreement dear reader! All the data is gotten somewhere, by someone and belongs to someone. Be a dear and pay attention to this even for a "toy" projects and prototypes: if, all of a sudden, you make up something cool, and then it turns out that data for this "cool" exists only under very strict and expensive commercial license (or, on the opposite, data that you need for a "business-idea" can only be used for non-commercial purposes) - it will be very unpleasant. For everyone.
Colors, gradients, painting
The first and basic (and, unfortunately, I guess, the only) choice of any rubist, who wants to paint something is a library Rmagick. "Unfortunately" – because this wrapper of a popular linux package ImageMagick has not very good reputation: it has API that is not always clear, doubtful productivity and, moreover, it is prone to memory leaks ... – but if you need to draw something quickly, you can just take and draw:
require 'rmagick'
img = Magick::Image.new(width, height) # image
canvas = Magick::Draw.new # subsidiary object for painting on the picture
# ...
canvas.polygon(*points) # painting itself
# ...
canvas.draw(img) # lay objects on the pictures
img.write('test.png')
When you get used to RMagick agreements, it gets very easy to solve our problem.
One last algorithmic task is left: to calculate the color of each country – gradient from one color to another. Once I had been solving this problem and, in fact, it was probably my first Contribution to the OpenSource™, but today, luckily, there is an excellent gem color, and with its help the code of choosing a color for every country depending on the numeric value gets very easy:
# here are passed:
# * the beginning and the end of the gradient in the format of CSS-colors: "#FF0000" or "white"
# * hash-dictionary country-numeric value
# * different options, you can find information about them in README
def draw_gradient(from_color, to_color, value_by_country, **options)
min = value_by_country.values.min
max = value_by_country.values.max
from = ::Color::RGB.by_css(from_color)
to = ::Color::RGB.by_css(to_color)
values = value_by_country.
map{|country, value| [country, value.rescale(min..max, 0..100)]}. # converting the value associated to each country to the range 0-100%
map{|country, value| [country, from.mix_with(to, 100 - value).html]}. # calculating the color for each percent
to_h
# now we have a hash country → color! we can draw
Finishing touches
Let's factor code as a couple of classes and modules and make it almost-true-gem.
Why "almost"? Because a Good Gem should have tests. And, to be honest, more clear and compact code. Also, it would be a good idea to connect rubocop, however, I can imagine what it would say. And it would be good to add some more painting methods.
Nevertheless, this is almost true gem:
- there is README with all the necessary parts – usage examples, beautiful pictures, explanation how and what this works for, the author and the license agreement mention etc.
- code is divided into
lib/
– the logic itself,bin/
– executables which you can run from command line in order to create your maps;examples/
– usage examples, which, by the way, also temporarily (smile) serve as tests; - there is accurately described
worldize.gemspec
, describing the gem; - the gem itself is posted on rubygems.org and can be installed using simple command
gem install worldize
.
Conclusion
Is it real?
To be honest - not quite. Despite the (brief) "spell of popularity", gem usability and helpfulness can be evaluated over longer periods of time. Worldize is just a draft so far, which – if it would be used, if it would receive bug reports, requests for features, pull-requests – can grow into something bigger. Or cannot grow because for an average website (even if its server side is written in Ruby) it is better to create such maps using a client library like d3.js that works in a browser and is interactive.
However, one day worldize can be of some use for scientific calculations in Ruby or PDF-reports. We'll see!
But what for?
... — a skeptical reader can wonder. It is clear that for a good programmer it is a pure pleasure to experiment with a random idea. But if the author himself says that "it is unlikely that the library would be used by someone as is" – what's all this fuss with refactoring the code, creating examples, formatting, posting for?..
Actually, this is a good habit: to imagine all your code as potentially public, as a code that all of a sudden can become useful for someone. A good Ruby-programmer is to a greater extent a writer, and writing "for the drawer" is bad for your self-esteem and self-development (so is writing at the request of a certain client). Also a lot of commercial organizations now prefer to generalize and deploy their code in the form of gems – which benefits both language/community and the organizations themselves (when a gem becomes popular and a lot of people make a contribution to its development).
Besides, "noticeability" in society is beneficial for a modern highly professional programmer – in case of changing the job or if, all of a sudden, you would Want to make Cool Open-Source and would need companions.
That's it.
This is an mkdev article written by Victor Shepelev. You can always hire Victor to be your personal Ruby mentor!
Top comments (0)