Как (почти) ничего не делая, попасть в Ruby-топ на GitHub


Однажды (вечером прошлого четверга) мне было скучно. В таких случаях я (иногда) пишу какой-нибудь экспериментальный код по взбрёвшей в голову вздорной идее. Если получается интересно — выкладываю на GitHub, иногда даже на RubyGems. На этот раз получилось, похоже, очень интересно: маленькая библиотека под глупым названием worldize за первые несколько дней своей жизни набрала сотню «звёздочек» на GitHub, четыре дня подряд была в топ-10 «репозиториев дня», попала в топ «репозиториев недели» и в популярную рассылку Ruby News Weekly.
И хотя мне немного обидно (ни одна из моих Важных и Серьёзных библиотек такого внимания никогда не получала), я всё же хочу рассказать, как эта штука получилась.
Идея и воплощение
Во многих статистических языках и пакетах есть возможность в одну строку построить карту мира, где каждая страна была бы раскрашена в цвет, соответствующий величине какого-нибудь значения (например, средней продолжительности жизни, или количеству панд на душу населения, или средней цене квартиры).
Например:
(Такие карты ещё называют хороплетами.)
Возникает идея: эй, а насколько сложно сделать такую штуку на Ruby? Есть идея, есть свободное время, есть Гугл Всемогущий — и вот результат:
require 'worldize'
Worldize::Countries.new.
draw_gradient(
'#D4F6C8', # градиент от этого цвета...
'#247209', # и до этого
# какое-то числовое значение для разных стран:
{'Argentina' => 1, 'Bolivia' => 2, 'Chile' => 3 .... }
)
Результат:
How cool iz dat?
Процесс
Порядок действий понятен с самого начала:
- найти в открытом доступе данные вида «название страны — координаты многоугольника этой страны»;
- научиться рисовать картинку с такими многоугольниками, закрашенными разными цветами;
- сделать простой API вокруг всего этого.
Записаться
Ты можешь сделать автора статьи
своим персональным наставником
Геоданные и карты
Нужные данные в свободном доступе¹ нашлись у Natural Earth, а их конвертация в удобную форму — в одном прекрасном репозитории. Удобная форма — это GeoJSON, который, в отличие от многих других картографических форматов, можно читать и использовать без дополнительных средств и библиотек.
Но это только полдела. Общее образование говорит нам, что полученные из таких датасетов геокоординаты — это координаты на шаре (мягко говоря), и чтобы преобразовать их в красивые многоугольники на плоскости нам нужна какая-то проекция. Вышеупомянутый Всемогущий Гугл сообщает, что самая популярная и узнаваемая — проекция WebMercator, используемая на современных онлайн-картах. После некоторого количества возни с формулами, указанными в Википедии, методом проб и ошибок получаем довольно наглядный Ruby-код:
# долгота в координату x картинки:
# просто переводим из одного диапазона в другой
# max_x — ширина ожидаемого изображения
x = lng.rescale(-180..180, 0..max_x)
# современный Ruby поддерживает имена переменных в Юникоде!
include Math
π = PI
φ = -lat * π / 180 # из градусов — в радианы, которых требует формула
# широта в координату y картинки
# формула с логарифмами и тангенсами даёт значение от −π до π
# и остаётся только перевести её из этого диапазона в диапазон «от 0 до высоты картинки»
y = log(tan(π / 4 + φ / 2)).rescale(-π..π, 0..max_y)
Полезный метод rescale
, который конвертирует число из одного диапазона в другой, определён в нашей библиотеке, причём используя фичу из 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
# теперь, в любом классе или модуле...
module TryMe
# ...где мы напишем using Refinements...
using Refinements
# ...у всех числовых переменных будет метод rescale
p 8.rescale(0..10, 0..100) # => 80
end
# а в других местах он не будет путаться в чужом коде
p 8.rescale(0..10, 0..100) # undefined method `rescale' for 8:Fixnum (NoMethodError)
Он, кстати, используется не только здесь, но и для расчёта градиентов цветов (см. ниже).
¹Про лицензию данных: дорогой читатель! Все данные откуда-то взялись, кем-то добыты и кому-то принадлежат. Будь хороший зайчик, обращай внимание на это даже для «игрушечных» проектов и прототипов: если вдруг ты придумаешь что-то крутое, а потом окажется что данные для этого «крутого» существуют только под жёсткой и дорогущей коммерческой лицензией (или, наоборот, данные которые тебе нужны для «бизнес-идеи» позволяют использование только в некоммерческих целях) — будет ужасно неприятно. Всем.
Цвета, градиенты, рисование
Первый и основной (и, к сожалению, примерно единственный) выбор любого рубиста, который хочет что-нибудь нарисовать — библиотека RMagick. «К сожалению» — потому что эта обёртка популярного линуксового пакета ImageMagick имеет не слишком хорошую репутацию: у неё не всегда понятный API, сомнительная производительность, и, кроме того, она склонна к утечкам памяти ... — но если вам нужно что-то быстро нарисовать, вы просто берёте и рисуете:
require 'rmagick'
img = Magick::Image.new(width, height) # изображение
canvas = Magick::Draw.new # вспомогательный объект для рисования на картинке
# ...
canvas.polygon(*points) # собственно, рисуем
# ...
canvas.draw(img) # наносим объекты на изображения
img.write('test.png')
Привыкнув к соглашениям RMagick, выполнить нашу задачу совсем несложно.
Остаётся последняя алгоритмическая задачка: рассчитать цвет каждой страны — градиент от начального цвета до конечного. Однажды я даже решал эту задачку и кажется это вообще был мой первый Контрибушен в Опен-Сорс™, но сегодня, к счастью, есть прекрасный гем color, и с использованием его код выбора цвета для каждой страны в соответствии с сопоставленным ей числовым значением становится совсем простым:
# сюда передаются:
# * начало и конец градиента в виде CSS-цветов: "#FF0000" или "white"
# * хеш-словарь страна-числовое значение
# * разные опции, о которых можно узнать в 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)]}. # конвертируем сопоставленное каждой стране значение в диапазон 0-100%
map{|country, value| [country, from.mix_with(to, 100 - value).html]}. # вычисляем цвет, соответствующий каждому проценту
to_h
# теперь у нас есть хеш страна → цвет! можно рисовать
Финальные штрихи
Оформляем код как пару классов и модулей и превращаем его в почти-настоящий-гем.
Почему «почти»? Потому что у Хорошего Гема должны быть тесты. И код, будем честны, почище и покомпактнее. И ещё rubocop неплохо бы подключить — я, впрочем, и так могу предположить, что бы он мне сказал. Да и опций рисования хорошо бы побольше добавить.
Тем не менее, это почти настоящий гем:
- есть README со всеми нужными частями — примеры использования, красивые картинки, объяснение как и зачем это работает, упоминание об авторе и лицензии и т.п.;
- код разложен на
lib/
— собственно, логику,bin/
— исполняемые файлы, которую можно вызвать из командной строки для построения своих карт;examples/
— примеры использования, которые, заодно, временно (смайлик) заменяют тесты; - есть аккуратно описанный
worldize.gemspec
, описывающий гем; - сам гем выложен на rubygems.org и может быть установлен простой командой
gem install worldize
.
Заключение
Оно настоящее?
Будем честны — не вполне. Несмотря на (мимолётный) «приступ популярности», используемость и полезность гема видна на бОльших промежутках времени. Worldize, пока что — только заготовка, которая — если она будет кем-то использоваться, получать новые багрепорты, запросы на фичи, пулл-реквесты — может вырасти во что-то полезное. А может и не вырасти — ведь для среднестатистического вебсайта (даже если его серверная часть написана на Ruby) такого рода карты лучше делать на клиентской библиотеке вроде d3.js — работающей в браузере и интерактивной.
Впрочем, для научных вычислений на Ruby, или для построения PDF-отчётов worldize может однажды пригодиться. Посмотрим!
А зачем?
... — может спросить скептический читатель. Понятно, что для хорошего программиста поэкспериментировать со случайной идеей — чистое удовольствие. Но если сам же автор говорит, что «вряд ли библиотеку в текущем виде кто-то будет использовать» — зачем вся эта суета с причёсыванием кода, созданием примеров, форматированием, выкладыванием?..
Вообще — это полезная привычка: о любом своём коде думать как о возможно публичном, о коде, который может внезапно кому-то понадобиться. Хороший Ruby-программист — это в большой степени писатель, и писать «в стол» — вредно для самооценки и саморазвития (как и писать только то, что потребовал конкретный заказчик). Да и многие вполне коммерческие организации сейчас предпочитают свой код обобщать и выкладывать в виде гемов — что приносит пользу и языку/сообществу, и самим организациям (когда гем становится популярный, и в его развитие вкладывается множество людей).
К тому же, современному высокопрофессиональному программисту полезна «заметность» в сообществе — и в случае смены работы, и, например, в случае если вам Очень Захочется сделать Крутой Open-Source, и понадобятся соратники.
Такие дела.
Мы рассказываем, как стать более лучшим разработчиком, как поддерживать и эффективно применять свои навыки. Информация о вакансиях и акциях эксклюзивно для более чем 8000 подписчиков. Присоединяйся!