Blog

Wyścig słupków: jak zrobić go dobrze

·6 min czytania
wizualizacja danychanimacjaJavaScript

Animowany wyścig słupków — bar chart race — to jeden z najczęściej oglądanych formatów wizualizacji. Ranking zmienia się w czasie, słupki wyprzedzają się nawzajem, widz zostaje do końca, żeby zobaczyć, kto wygra.

Wygląda banalnie. Potem próbujesz zrobić własny i okazuje się, że prawie każda decyzja ma podwójne dno. Poniżej to, czego nauczyłem się przy animacji pokazującej liczbę sklepów sieci handlowych w Polsce od 1990 do 2026 roku.

Trzy drogi

Gotowy generator. Flourish albo podobne narzędzie: wrzucasz arkusz, dostajesz film. Zaletą jest czas — kwadrans od danych do wyniku. Wadą to, że wynik wygląda dokładnie jak tysiąc innych, a nad niczym nie masz kontroli: ani nad kolorem, który mógłby coś znaczyć, ani nad tym, co się dzieje między pomiarami.

Biblioteka. D3 z własną pętlą animacji, bar_chart_race w Pythonie, gganimate w R. Kontrola rośnie, ale wraz z nią liczba decyzji, które musisz podjąć samodzielnie — i to o nich jest reszta tego wpisu.

Własny renderer. Rysowanie na canvas klatka po klatce i złożenie ich w film. Najwięcej pracy, pełna kontrola. Wybrałem tę drogę i nie żałuję, ale nie dlatego, że lubię pisać kod od zera — tylko z powodu, który opisuję niżej.

Renderuj klatki, nie nagrywaj ekranu

Naturalny odruch to uruchomić animację i nagrać ekran. To błąd, który mści się cicho: nagranie łapie tyle klatek, ile zdąży, więc tempo faluje, a przy dłuższych filmach pojawia się dryf.

Lepiej odwrócić zależność. Zamiast pozwolić animacji płynąć, ustawiasz moment i prosisz o rysunek:

for (let i = 0; i < liczbaKlatek; i++) {
  const t = start + (koniec - start) * (i / liczbaKlatek);
  await strona.evaluate((t) => rysujKlatke(t), t);
  await strona.screenshot({ path: `klatka-${i}.png` });
}

Sterowanie przeglądarką bierze na siebie Playwright, składanie filmu — ffmpeg. Efekt jest deterministyczny: ta sama data wejściowa daje bit w bit tę samą klatkę, a podgląd w przeglądarce pokazuje dokładnie to, co trafi do pliku.

Warunek jest jeden i trzeba go pilnować: funkcja rysująca nie może pamiętać niczego z poprzedniej klatki. Żadnego stanu, żadnej animacji „od bieżącej pozycji do nowej". Wszystko musi wynikać z samego t.

Interpolacja, która nie zmyśla historii

Dane rzadko są miesięczne. Masz raport roczny, wzmiankę prasową, potem dwuletnią lukę. Animacja potrzebuje wartości dla każdej klatki, więc trzeba je czymś wypełnić — i tu zaczyna się odpowiedzialność.

Zwykły spline poprowadzony przez rosnące punkty potrafi zejść poniżej obu. Na wykresie liczby sklepów oznacza to zdanie, którego nikt nie wypowiedział: „sieć zamknęła dwieście sklepów i zaraz je odbudowała". Nikt tego nie napisał, a jednak widz to zobaczy.

Rozwiązaniem jest interpolacja monotoniczna — PCHIP. Między dwoma rosnącymi punktami przebieg nie zawróci, bo z definicji nie może.

Druga zasada dotyczy końca zbioru: poza ostatnim pomiarem wartość przytrzymujemy, nie przedłużamy trendu. Ekstrapolacja jest dopisywaniem danych, których nie ma — dokładnie tym, czego taki wykres powinien unikać.

Warto też rozróżnić, skąd pochodzi każda wartość: pomiar źródłowy, interpolacja między pomiarami, czy ostatnia znana wartość przeniesiona dalej. W moim zbiorze każdy miesiąc ma taką etykietę i można nią sterować stylem linii — widz z ciekawością zobaczy różnicę, recenzent doceni uczciwość.

Ranking, który nie skacze

Tu spędziłem najwięcej czasu i tu każdy poradnik milczy.

Ranking jest z natury dyskretny: albo jesteś trzeci, albo czwarty. Gdy dwie sieci mają zbliżone wartości, kolejność potrafi zmieniać się co miesiąc, a słupki drgają zamiast się mijać.

Odruch pierwszy: porównywać wartości „miękko", z tolerancją. Efekt uboczny jest fatalny — dwie sieci różniące się o piętnaście sklepów sklejają się na stałe, bo różnica mieści się w strefie rozmycia.

Odruch drugi: uśredniać pozycję w czasie. To jest właściwy kierunek — liczę ranking w dziewięciu momentach wokół t i uśredniam oknem Hanna. Z dala od skrzyżowania pozycje wychodzą całkowite, a w chwili mijania słupki przechodzą przez siebie płynnie.

Ale samo uśrednianie nie wystarczy. Dwa dodatkowe warunki:

Próg remisu. Gdy dwie sieci idą łeb w łeb przez cały rok, dyskretny ranking skacze w każdej próbce, a uśrednienie daje obu pozycję „mniej więcej w połowie" — czyli jeden słupek narysowany na drugim. Różnice mniejsze niż ułamek procenta skali traktuję więc jako remis rozstrzygany stałym indeksem.

Uwaga na okno wychodzące poza zakres. Okno uśredniania sięga przed i za t. Jeśli sieć jeszcze nie istniała, próbki sprzed jej startu dają zero i wypychają ją na koniec stawki — rozmywając pozycje wszystkim poniżej. Rozwiązanie: do ustalania kolejności przycinaj czas do zakresu istnienia serii, zamiast zerować wartość.

Tempo powinno wynikać z długości

Kuszące jest wystawienie osobnego parametru „szybkość przejść". Lepszym rozwiązaniem jest powiązanie okna wygładzania z jednostkami danych, nie z sekundami nagrania.

U mnie okno rankingu liczone jest w latach danych. Dzięki temu wydłużenie filmu z czterdziestu do osiemdziesięciu sekund automatycznie spowalnia każdą zamianę miejsc w tej samej proporcji. Jeden parametr mniej i jedna klasa niespójności mniej.

Kolor niech coś znaczy

Typowy wyścig słupków maluje każdą serię innym kolorem z palety generatora. Kolor nie niesie wtedy żadnej informacji — jest wyłącznie etykietą, a nazwa i tak stoi obok.

Ta sama przestrzeń może pracować. W moim wykresie kolor koduje trzy rzeczy, których nie widać w samych liczbach: model biznesowy (własny kontra franczyza), inną branżę przy podmiotach nieporównywalnych wprost, oraz wyjście z rynku. Lider dostaje jedyny akcent w kadrze.

Widz rozróżnia serie po nazwie, a kolorem dostaje warstwę interpretacji: że dwie rosnące krzywe rosną innym mechanizmem i że niektóre słupki nie tylko przestają rosnąć, ale znikają.

Błędy widać na klatce, nie w kodzie

Najważniejszy wniosek z całej pracy.

Wszystkie trzy problemy z rankingiem — sklejanie, drganie przy remisie, wypychanie serii spoza zakresu — przechodziły testy logiczne. Wartości się zgadzały, sumy się zgadzały, funkcje zwracały to, czego oczekiwał test. Objaw był widoczny wyłącznie na obrazku: dwa słupki narysowane jeden na drugim.

Jeśli robisz coś podobnego, wygeneruj kilka klatek kontrolnych w newralgicznych momentach i obejrzyj je, zanim puścisz renderowanie całości. Kilka sekund pracy oszczędza kwadrans czekania na film, w którym w trzydziestej sekundzie coś się nakłada.

Osobnego przebiegu wymagają też etykiety. Jeśli stoją w stałej kolumnie, przy ciasnym rankingu drukują się jedna na drugiej — u mnie dotyczyło to jednej czwartej filmu, zanim dodałem rozsuwanie.

Czego wyścig słupków nie potrafi

Na koniec uczciwie, bo to format chwalony ponad miarę.

Wyścig słupków jest słabym wykresem analitycznym. Pokazuje kolejność w danej chwili, ale utrudnia porównanie tempa wzrostu, ukrywa wartości bezwzględne za animacją i nagradza serie o dużych liczbach niezależnie od tego, czy porównanie ma sens. Do odpowiedzi na pytanie „jak szybko to rosło" znacznie lepszy jest zwykły wykres liniowy — nudny i czytelny.

Wyścig sprawdza się tam, gdzie historia jest o zmianie kolejności: kto kogo wyprzedził, kiedy i jak długo się to działo. Jeśli twoja historia jest o czymś innym, animacja doda ruchu, ale odbierze zrozumienie.


Kod, dane i pełny opis metodologii przygotowuję do publikacji razem z materiałem wideo. Wszystkie liczby pochodzą ze źródeł publicznych — raportów spółek i prasy branżowej — a każdy punkt ma przypisany adres źródła.