Prosta infografika

Jakiś czas temu parę osób pytało jak się robi infografiki — takie jak w moim dawnym wpisie na blogu, gdzie między innymi można było zobaczyć różnice w zachowaniach młodzieży w różnych państwach Europy na podstawie obserwacji lokalnych znaków drogowych. Infografiki składały się zasadniczo z dwóch części — mapy i legendy. Niestety, nie dokumentowałem na bieżąco całego procesu powstawania tamtej wprawki, pełnego błędów i ślepych zaułków, więc wszystko teraz wyda się o wiele łatwiejsze niż było w rzeczywistości.

Celem opisanych tu działań jest utworzenie infografiki w postaci pliku HTML, który bez problemu powinna pokazać każda przeglądarka WWW. Do tego potrzebną są odpowiednie materiały i narzędzia. Materiały to:

Narzędzia to:

  • edytor tekstu — zwykły prosty edytor tekstu, na przykład znany niektórym użytkownikom Windows Notatnik
  • przeglądarka WWW
  • Interpreter języka programowania Python

W Pythona trzeba zaopatrzyć się samemu jeżeli korzysta się z Windows — OSX i Linux ma go standardowo zainstalowanego. Dystrybucja Pythona, z której korzystam, to Anaconda, którą można (darmowo i legalnie) pobrać ze strony plików do pobrania Continuum Analytics. Wybierz tam swój system operacyjny i wersję Pythona — 3.5 (lub wyższą). Nie pobierz przez pomyłkę którejś wersji z serii 2.x — podane dalej przykłady nie będą poprawnie działały. Odpal instalator, parę razy kliknij gdzie trzeba i gotowe. Anaconda zawiera wszystkie biblioteki, których używam w tych przykładach, więc zaraz po zainstalowaniu będzie można zabrać się do pracy.

Ponieważ projekt będzie składał się z kilku czy nawet kilkudziesięciu plików warto sobie przygotować miejsce do pracy — czyli katalog (po windowsowemu nazywa się to „folder). Na tym etapie nie ma znaczenia jak go nazwiesz i gdzie się będzie znajdował, ale na potrzeby tego tekstu będę go nazywał „infog”. W tym katalogu znajdą się pliki, które będziemy tworzyli — plik strony w postaci HTML oraz nasze programy w Pythonie. Zadbaj też o to, by w katalogu znalazł się plik ze ściągniętą mapą.

Zaczniemy od utworzenia najprostszej możliwej strony zawierającej jedynie pobraną mapę Europy. Cały kod strony może wyglądać tak:

<html>
<head>
<title>
Moja pierwsza infografika
</title>
</head>
<body>
<img src='mojamapa.svg'>
</body>
</html>

Możesz ten kod przekleić do edytora tekstu i zapisać w katalogu „infog” np. jako „wprawka.htm”, choć oczywiście masz pełne prawo do wybrania własnej nazwy. Zrób kopię „Blank_map_of_Europe_2.svg” o nazwie „mojamapa.svg” (oczywiście w katalogu „infog”). Teraz w swojej przeglądarce otwórz plik „wprawka.htm”. Jeżeli wszystko poszło dobrze to zobaczysz szarą mapę Europy. Czas zabrać się za kolorowanie.

Zacznijmy od pokolorowania jednego państwa — Polski — na zielono. Poniższy kod to wszystko, czego do tego potrzebujesz. Skopiuj go do edytora tekstu i zapisz w katalogu „infog” pod nazwą np. „koloruj.py”.

from bs4 import BeautifulSoup

dcolor= '192, 192, 192'
dstroke='55, 55, 55'
dstroke_width = '0.5'

svg = open('Blank_map_of_Europe_2.svg', 'r').read()
soup = BeautifulSoup(svg, 'xml')
paths = soup.find_all('path')

for p in paths:

    acolor = '192, 192, 192' # assume default color

    if p['id'] == 'pl':
        acolor = '0, 255, 0'

    mstyle = 'fill: rgb(' + acolor + '); stroke: rgb(' + dstroke + '); stroke-width: '+dstroke_width+'; fill-opacity:1'
    p['style'] = mstyle
      
svg = open('mojamapa.svg', 'w')
svg.write(soup.prettify(formatter=None))
svg.close()

Co tu robimy? Na poczatku wczytujemy z dysku mapę (linia 7) i tłumaczymy do postaci nadającej się do dlaszej obróbki (linie 8 i 9). Tworzymy listę wszystkich obszarów na mapie i nadajemy jej nazwę „paths”. Na szczęście twórcy mapy zadbali o to, by każdy obszar był oznaczony dwuliterowym kodem państwa zgodnie z normą ISO 3166-1, co bardzo ułatwia dalsze działania. Od lini 11 przelatujemy przez wszystkie elementy listy „paths” (pojedynczy element nazywamy sobie „p”) i sprawdzamy, czy odpowiada potrzebnemu nam kodowi państwa — w tym przypadku ‚pl’. Jeżeli tak, to zamieniamy kolor podstawowy (ustawiony w linii 13) na nasz ulubiony zielony.

Aby wykonać program musisz wejść do wiersza poleceń (w Windows poleceniem „cmd”), przejść do katalogu „infog”, a następnie wpisać polecenie „python koloruj.py”. Jeżeli wszystko poszło dobrze to po przejściu do przeglądarki i odświeżeniu strony „wprawka.htm” powinna się ukazać szara mapa Europy z zieloną Polską:

zielona_polska

Program zrobił to co miał zrobić, ale jest do kitu, i to z wielu powodów. Jeżeli by trzeba było w taki sposób pokolorować więcej niż jedno państwo trzeba by wstawiać całą masę linijek typu:

    if p['id'] == 'fr':
        acolor = '0, 0, 255' 
    if p['id'] == 'de':
        acolor = '255, 0, 0'

To by nam dało niebieską Francję i czerwone Niemcy, ale tak napuchnięty kod jest brzydki i trudno jest go modyfikować. Co prawda można go łatwo rozbudowywać metodą kopiuj-wklej, ale wtedy coraz łatwiej o pomyłki, których szukanie może każdego zniechęcić do tej zabawy.

Warto też pamiętać o jednym: mamy całą masę państw do pokolorowania na zaledwie trzy kolory (dzieci idą, dzieci biegną, dzieci pędzą). Tak czy tak musimy stworzyć listy państw w danej kategorii:

children_walk = ['at', 'be', 'cz', 'dk', 'gr', 'ie', 'no', 'sk', 'se', 'tr']
children_run = ['fr', 'de', 'hu', 'is', 'hu', 'nl', 'pl', 'ch', 'uk']
children_dash = ['ee', 'it', 'ru', 'by', 'es', 'ua']

Jeżeli w przyszłości dajmy na to Francja przyspieszy swoje dzieci to wystarczy usunąć ‚fr’ z listy children_walk i dopisać ją do children_run.

Teraz sprawdzamy już nie to, czy ‚id’ jest równe kodowi państwa lecz to, czy jest w danej liście państw. Jeżeli jest to kolorujemy, jeżeli jie to idziemy dalej. Klasycznie postąpilibyśmy teraz tak:

if p['id'] in children_walk:
    acolor = '0, 255, 0' #zielony
elif p['id'] in children_run:
    acolor = '255, 255, 0' #żółty
elif p['id'] in children_dash:
    acolor = '255, 0, 0' #czerwony

To już o tyle lepsze, że przesunięcie państwa z jednej grupy do drugiej nie będzie wymagało grzebania w kodzie przypisującym kolory, ale i tak nie wyglada to dobrze. Ogólna zasada mówi, że jeżeli linie kodu wyglądają bardzo podobnie to da sie je zastapić jakąś prostszą konstrukcją, a prosty kod łatwiej jest rozwijać i zmieniać. Poza tym ograniczamy się teraz do trzech różnych kategorii, a nie wiadomo kiedy będzie nam potrzeba ich więcej lub mniej. Warto więc rozdzielić ustawianie kolorów państw od kolorowania mapy.

W tym celu utworzymy jeden centralny słownik, początkowo pusty, który nazwiemy „colors”. W miarę potrzeb bedziemy go wypełniali kolorami, a na koniec wykorzystamy do pokolorowania mapy:

from bs4 import BeautifulSoup

dcolor= '192, 192, 192'
dstroke='55, 55, 55'
dstroke_width = '0.5'

colors = {}

children_walk = ['at', 'be', 'cz', 'dk', 'gr', 'ie', 'no', 'sk', 'se', 'tr']
children_run = ['fr', 'de', 'hu', 'is', 'hu', 'nl', 'pl', 'ch', 'uk']
children_dash = ['ee', 'it', 'ru', 'by', 'es', 'ua']

for c in children_walk:
    colors[c] = '0, 255, 0' #zielony

for c in children_run:
    colors[c] = '255, 255, 0' #żółty

for c in children_dash:
    colors[c] = '255, 0, 0' #czerwony

svg = open('Blank_map_of_Europe_2.svg', 'r').read()
soup = BeautifulSoup(svg, 'xml')
paths = soup.find_all('path')

for p in paths:

    acolor = '192, 192, 192' # assume default color

    if p['id'] in colors:
        acolor = colors[p['id']]

    mstyle = 'fill: rgb(' + acolor + '); stroke: rgb(' + dstroke + '); stroke-width: '+dstroke_width+'; fill-opacity:1'
    p['style'] = mstyle
      
svg = open('mojamapa.svg', 'w')
svg.write(soup.prettify(formatter=None))
svg.close()

Od tej chwili nie musimy już za bardzo grzebać w pętli kolorującej (cały wcięty obszar kodu począwszy od „for”), cała zabawa z ustawianiem kolorów zostaje na początku programu. Przy okazji pisania tego tekstu odkryłem, że przegapiłem Finlandię, Portugalię i Rumunię. Teraz wystarczy je dopisać do odpowiedniej listy. Zamiast

children_walk = ['at', 'be', 'cz', 'dk', 'gr', 'ie', 'no', 'sk', 'se', 'tr']
children_run = ['fr', 'de', 'hu', 'is', 'hu', 'nl', 'pl', 'ch', 'uk']
children_dash = ['ee', 'it', 'ru', 'by', 'es', 'ua']

będziemy mieli

children_walk = ['at', 'be', 'cz', 'dk', 'gr', 'ie', 'no', 'sk', 'se', 'tr', 'fi']
children_run = ['fr', 'de', 'hu', 'is', 'hu', 'nl', 'pl', 'ch', 'uk']
children_dash = ['ee', 'it', 'ru', 'by', 'es', 'ua', 'pt', 'ro']

Lista na Wikipedii nie uwzględnia Litwy i Łotwy — zapewne są takie same jak w innych byłych republikach ZSRR, ale nie sprawdzałem więc państwa te pozostają szare.

Ponowne odpalenie „koloruj.py” i odświeżenie strony da nam taki efekt:

kidsrun_almost_there_but_ru_and_uk

Wygląda to już dość dobrze, ale są pewne oczywiste baboki. Chociaż wpisaliśmy dane dla Rosji (‚ru’) i Zjednoczonego Królestwa (‚gb’), oba te państwa pzostały szare. Dlaczego?

W tej sytuacji warto zorientować się, jakie zawarte w mapie obszary nie zostały pokolorowane. Skoro przelatując przez obszary wyszukujemy ich ‚id’ w słowniku ‚colors’ warto kazać programowi informować nas, że dany obszar nie został znaleziony:

    if p['id'] in colors:
        acolor = colors[p['id']]
    elif:
        print(p['id'])

Te informacje pojawią się w oknie wiersza poleceń i będą wyglądały mniej więcej tak:

kidsrun_errors_001

Kilka pozycji od razu rzuca się w oczy — ‚ru-main’, ‚ru-kgd’, ‚gb-nir’, ‚gb-gbn’ oraz ‚large_masses_of_water’. Oczywiście ‚ru-main’ to główny obszar Rosji, a ‚ru-kgd’ to Obwód Kaliningradzki; Wielka Brytania zaś podzielona jest na część główną oraz Irlandię Północną. Podstawowy problem z Wielką Brytanią polega na tym, że standardowo nazywa się „Zjednoczonym Królestwem”, którego kodu użyliśmy w programie. Najprościej byłoby podmienić na liście ‚uk’ na ‚gb’, ale taki zabieg to samo zło — nasze oryginalne dane zostaną przekłamane. Zawsze warto jest mieć osobne miejsce na poprawne dane, a osobne na wyjątki. Tu rozwiązaniem jest dodanie nowej pozycji ‚gb’ do słownika ‚colors’ i nadanie jej wartości z ‚uk’, najlepiej zaraz po zdefiniowanych listach kolorów (a na pewno nie przed, bo wtedy ‚uk’ będzie jeszcze niezdefiniowane):

colors['gb'] = colors['uk']

To jednak nie rozwiązuje problemu kodów dłuższych niż dwuliterowe. Praktycznym rozwiązaniem będzie sprawdzanie nie całego ‚id’, ale tylko jego pierwszych dwóch znaków. Zamiast

    if p['id'] in colors:
        acolor = colors[p['id']]

napiszemy

    if p['id'][:2] in colors:
        acolor = colors[p['id'][:2]]
    else:
        print(p['id'])

To powtórzenie ‚p[‚id’][:2]’ też jest dość brzydkie więc dla jasności jeszcze zmodyfikujemy kod tak:

    country_id = p['id'][:2]
    if country_id in colors:
        acolor = colors[country_id]
    else:
        print(p['id'])

Celowo po ‚else’ nie wykorzystałem ‚country_id’, bo zawiera ono tylko pierwsze dwa znaki, a przy odpluskwianiu interesuje nas maksimum informacji.

Właściwie kolorowanie mapy jest już zakończone. Został jeszcze jeden wkurzający drobiazg, owe „large_masses_of_water”, czyli co większe jeziora, których na porządnym kartogramie nie wypada przedstawiać. Po prostu wywalmy je (w liniach 41-42 poniższego kodu). Ostateczny kod (przynajmniej na chwilę – jedyny ostateczny kod to martwy kod) wygląda tak:

from bs4 import BeautifulSoup
 
dcolor= '192, 192, 192'
dstroke='55, 55, 55'
dstroke_width = '0.5'
 
colors = {}
 
children_walk = ['at', 'be', 'cz', 'dk', 'gr', 'ie', 'no', 'sk', 'se', 'tr', 'fi']
children_run = ['fr', 'de', 'hu', 'is', 'hu', 'nl', 'pl', 'ch', 'uk']
children_dash = ['ee', 'it', 'ru', 'by', 'es', 'ua', 'pt', 'ro']
 
for c in children_walk:
    colors[c] = '0, 255, 0' #zielony
 
for c in children_run:
    colors[c] = '255, 255, 0' #żółty
 
for c in children_dash:
    colors[c] = '255, 0, 0' #czerwony
    
colors['gb'] = colors['uk']    
 
svg = open('Blank_map_of_Europe_2.svg', 'r').read()
soup = BeautifulSoup(svg, 'xml')
paths = soup.find_all('path')
 
for p in paths:
 
    acolor = '192, 192, 192' # assume default color
    
    country_id = p['id'][:2]
    if country_id in colors:
        acolor = colors[country_id]
    else:
        print(p['id'])
 
    mstyle = 'fill: rgb(' + acolor + '); stroke: rgb(' + dstroke + '); stroke-width: '+dstroke_width+'; fill-opacity:1'
    p['style'] = mstyle

    if p['id'] == 'large_masses_of_water':
        p.decompose()

       
svg = open('mojamapa.svg', 'w')
svg.write(soup.prettify(formatter=None))
svg.close()

Mamy już mapę, ale strona nie zawiera jeszcze nic poza mapą, więc wróćmy do pliku „wprawka.htm”. Na początek dodajmy tytuł:

<html>
<head>
<title>
Moja pierwsza infografika
</title>
</head>
<body>
<h1>Prędkości dzeci na przejściach dla pieszych</h1>
<img src='mojamapa.svg'>
</body>
</html>

Jest duże prawdopodobieństwo, że zobaczysz coś takiego:

kidsrun_bad_coding

Pomimo tego, że to Polacy wymyślili Internet i WWW jakieś 150 lat temu, niektóre przeglądarki nie radzą sobie dobrze z pokazywaniem znaków innych niż z zestawu anglosaskiego, i trzeba je poinstruować jak mają sobie z tym poradzić. Dodaj w sekcji ‚head’ jedną linijkę informującą o sposobie kodowania, a wszystko będzie OK.

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>
Moja pierwsza infografika
</title>
</head>
<body>
<h1>Prędkości dzeci na przejściach dla pieszych</h1>
<img src='mojamapa.svg' width='1000'>
</body>
</html>

Teraz czas zabrać się za legendę mapy i przedstawienie wszystkich znaków, na wypadek ewentualnych sporów czy w danym państwie dzieci jeszcze tylko szybko idą czy już biegną. Oraz analizę gender owych znaków — taką jak w omawianym wpisie. Oraz liczenie spadających kamieni. Ale to wszystko mnie tak wyczerpało, że spadam. Jak kamień. Ciąg dalszy przy najbliższej okazji.


Masz pytania? Uwagi? Problemy z podanym kodem? Nie potrafisz skopiować? Zapisac pliku na dysku? Otworzyć pliku w przeglądarce? Pytaj. Nie ma głupich pytań. Kontaktuj się w komentarzach po tym wpisem albo na moim Twitterze.

Reklamy

Jedna odpowiedź to “Prosta infografika”

  1. 5000lib Says:

    Cześć, Dzięki za tekst o infografice, przeczytam w weekend, teraz mam urwanie głowy, pisz, pisz. Pozdrawiam i do pisania.

Tu możesz zostawić komentarz, ale może najpierw przeczytaj "zasady komentowania".

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s


%d blogerów lubi to: