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

Kak pochti nichego ne delaya popast v ruby top na 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 .... }
  )

Результат:

Хороплет worldize

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, и понадобятся соратники.

Такие дела.

worldize первый на GitHub


Наконец-то решил заняться самообразованием?

Тогда начни с нашего бесплатного путеводителя по миру веб-разработки. Внутри куча полезных советов и материалов для самостоятельного изучения.

Заполучить книгу Cover huge ru