Projektowanie simple-ascii-chart: czego wykresy w terminalu nauczyły mnie o projektowaniu API

  • typescript
  • cli
  • library-design
  • terminal
  • npm

Praktyczny esej o ograniczeniach renderowania w terminalu, ergonomii API, projektowaniu workflow w CLI, wejściu strumieniowym i kompromisach stojących za simple-ascii-chart oraz simple-ascii-chart-cli.

Zbudowałem simple-ascii-chart z bardzo prozaicznego powodu: chciałem szybciej dostawać odpowiedzi w terminalu.

Kiedy i tak już jestem w shellu, zwykle nie chcę dashboardu. Chcę wiedzieć, czy użycie CPU skacze, czy skrypt zwalnia, czy licznik powoli idzie w górę, albo czy proces zachowuje się stabilnie w czasie. W takim momencie otwieranie przeglądarki wydaje się zbyt ciężkie. Czytanie surowych liczb działa, ale jest wolniejsze niż zobaczenie kształtu sygnału.

Narzędzia terminalowe świetnie nadają się do testowania założeń, bo bardzo szybko obnażają kompromisy. W przeglądarce można schować się za pikselami. W terminalu każdy znak staje się decyzją produktową.

Ten wpis jest przeglądem decyzji, które okazały się najważniejsze: domyślnych ustawień, które utrzymały czytelność wykresów, "wyjść awaryjnych", które nie rozdymały API, oraz decyzji w CLI, dzięki którym narzędzie dobrze składa się w prawdziwych pipeline'ach.

To, czego szukałem, było zaskakująco trudno znaleźć w jednym miejscu:

  • mała biblioteka TypeScript z prostym modelem wejścia
  • wynik, który pozostaje czytelny w prawdziwym terminalu
  • CLI, które dobrze odnajduje się w potokach i shellowych one-linerach
  • projekt na tyle mały, żeby mógł zachować wyraźne założenia

Dlatego to stało się dwoma narzędziami, a nie jednym.

Wypróbuj w 60 sekund

Jeśli chcesz tylko zobaczyć, co to robi:

# Przykład: punkty rozdzielane spacjami ze stdin (statycznie) printf '1 12\n2 18\n3 10\n4 20\n5 16\n' \ | simple-ascii-chart --format space --title "Build time (ms)" --height 8

A dla strumienia na żywo:

# Przykład: strumień liczb z ruchomym oknem some_command_that_prints_numbers \ | simple-ascii-chart --stream --window 60 --height 10

(Możesz dostosować zakresy osi, miejsce renderowania i parsowanie, ale celem nadal jest użyteczność w one-linerach.)

Dlaczego powstały zarówno biblioteka, jak i CLI

simple-ascii-chart to warstwa biblioteczna. Renderuje wykresy jako zwykły tekst terminalowy i jest przeznaczona do skryptów, narzędzi wewnętrznych, logów, szybkiej diagnostyki i wszędzie tam, gdzie wykres w przeglądarce byłby zbędnym narzutem.

Rdzeniowy model danych jest celowo mały. Pojedyncza seria to tablica punktów [x, y]. Wiele serii to po prostu tablica takich serii. Ten kształt łatwo zapamiętać, łatwo wygenerować i łatwo przenosić między prawdziwym kodem a szybkimi przykładami.

Biblioteka wspiera wystarczająco szeroki zakres, żeby pozostać użyteczna, w tym kolory, legendy, progi, punkty nakładane, własne formatery, nadpisywanie symboli oraz kilka trybów, takich jak line, point, bar i horizontalBar.

simple-ascii-chart-cli rozwiązuje inny problem. Gdy jesteś już w terminalu, nie chcesz pisać kodu tylko po to, żeby zobaczyć trend. Chcesz komendy, która działa w pipeline'ie i nie wchodzi w drogę.

Dlatego CLI zależy od biblioteki zamiast duplikować renderer. Logika rysowania pozostaje wielokrotnego użytku, a CLI skupia się na workflow w shellu: czytaniu z --input, --input-file, statycznego stdin albo strumieniowego stdin oraz szybkim zamienieniu tego wejścia na wykres. Główna komenda to simple-ascii-chart, a paczka trzyma też simple-ascii-chart-cli jako alias dla kompatybilności.

Ten podział nadal wydaje mi się słuszny:

  • używaj biblioteki, gdy piszesz kod
  • używaj CLI, gdy i tak już jesteś w shellu

Wokół obu paczek zbudowałem też małą stronę z przykładami, dokumentacją, playgroundem i endpointem API. To nie jest główny temat tego wpisu, ale bardzo ułatwiło eksplorowanie, testowanie i wyjaśnianie projektu.

Terminal wymusza większą dyscyplinę niż przeglądarka

Renderowanie w terminalu jest bezlitosne.

Wykres w przeglądarce może ukryć wiele rzeczy za pikselami, odstępami, stanami hover, zróżnicowaniem kolorów i dodatkowymi warstwami wizualnymi. Wykres terminalowy nie ma nic z tych rzeczy. Każda etykieta konkuruje z wykresem. Każdy symbol zajmuje realne miejsce. Każda wartość domyślna natychmiast staje się widoczna.

Kilka ograniczeń ostatecznie ukształtowało prawie każdą decyzję projektową:

  • dostępna szerokość i wysokość są zwykle małe
  • słownik środków wizualnych jest niewielki
  • etykiety konkurują bezpośrednio z obszarem wykresu
  • decyzje o skalowaniu mogą sprawić, że wykres będzie czytelny albo mylący
  • każda dodatkowa funkcja dodaje szum poznawczy

Ten ostatni punkt okazał się ważniejszy, niż się spodziewałem. W narzędziach terminalowych funkcja nigdy nie jest "tylko" funkcją. Jeśli nie zasłuży na swoje miejsce, staje się bałaganem. Utrudnia czytanie wykresu, utrudnia zapamiętanie API, albo jedno i drugie.

To popchnęło mnie w stronę powściągliwości dużo bardziej niż rozbudowy.

API, po które chciałem sięgać

Najciekawszą częścią projektu nie był sam renderer. Najciekawsze okazało się pytanie, jakim narzędziem to ma być, kiedy pojawiają się prawdziwe ograniczenia.

Po pierwsze, utrzymałem mały model danych, żeby skrócić skok od "mam wartości" do "widzę trend". Tablica punktów [x, y] nie jest szczególnie nowatorska, ale łatwo utrzymać ją w głowie, łatwo serializować i łatwo generować z prawdziwego kodu albo z ad hocowych skryptów.

Po drugie, potraktowałem domyślne ustawienia jako część produktu. Wczesne wersje zbyt mocno szły w elastyczność, bo od strony implementacji każda opcja wyglądała sensownie. Z zewnątrz zbyt wiele opcji natychmiast tworzy tarcie. Ostatecznie przyjąłem bardziej rygorystyczną zasadę: pierwszy wykres ma być łatwy do zrobienia, a personalizacja ma istnieć tylko tam, gdzie wyraźnie poprawia realny przypadek użycia.

Po trzecie, zostawiłem wyjścia awaryjne, ale bez zamieniania całej biblioteki w ścianę konfiguracji. Dlatego biblioteka wspiera własne symbole, formatowanie osi, legendy, progi, punkty i niższopoziomowe haki takie jak lineFormatter, ale nadal stara się utrzymać niewielki interfejs główny.

Po czwarte, pakietowanie jest częścią doświadczenia. Liczy się ergonomia importów. Liczy się instalacja. Liczy się pierwsze uruchomienie. Małe narzędzie powinno być czymś, po co łatwo sięgnąć.

// ESM import { plot } from 'simple-ascii-chart';
// CommonJS const { plot } = require('simple-ascii-chart');

Taki styl użycia biblioteki chciałem zoptymalizować:

import { plot } from 'simple-ascii-chart'; const chart = plot( [ [1, 12], [2, 18], [3, 10], [4, 20], [5, 16], ], { title: 'Build time (ms)', width: 24, height: 8, color: 'ansiCyan', }, ); console.log(chart);

Dokładny wynik będzie się różnił w zależności od szerokości terminala i ustawień, ale cel pozostaje ten sam: minimalny kształt wejścia, sensowne wartości domyślne i natychmiastowy "kształt sygnału".

CLI to moment, w którym projekt stał się praktyczny

Biblioteka jest przydatna, kiedy już wiesz, że chcesz pisać kod. CLI jest przydatne, kiedy patrzysz na terminal i chcesz odpowiedzi teraz.

Ostatecznie zaprojektowałem CLI wokół dwóch workflow:

  1. Plot this once - sparsuj niewielki payload ze stdin lub pliku i wypisz wykres.
  2. Keep plotting - strumieniuj liczby, utrzymuj ruchome okno i przerysowuj wykres w kontrolowanym tempie.

Większość flag wpada więc do czterech kategorii:

  • parsowanie wejścia (--format, --input, --input-file)
  • kontrola strumieniowania (--stream, --window, --refresh-ms)
  • kompozycyjność (--passthrough, --plot-output)
  • drobne ułatwienia (--rate, --series)

Jeden szczegół okazał się ważniejszy, niż się spodziewałem: stdout kontra stderr. W trybie passthrough surowy strumień może pozostać widoczny na stdout, podczas gdy żywy wykres renderuje się na stderr. To drobiazg, ale dokładnie taki drobiazg decyduje o tym, czy CLI naprawdę składa się w prawdziwych workflow w shellu.

Obsługa błędów podąża za tą samą logiką przyjazną one-linerom: brak wejścia, nieprawidłowe wymiary i zepsuty JSON powinny kończyć się szybko krótkimi komunikatami, podczas gdy niepoprawne dodatki, takie jak progi albo punkty, mogą wyświetlić ostrzeżenie i mimo to wyrenderować wykres.

Ten mały przykład ze stdin dobrze oddaje doświadczenie, które chciałem osiągnąć:

printf '1 12\n2 18\n3 10\n4 20\n5 16\n' \ | simple-ascii-chart --format space --title "Build time (ms)" --height 8

Jeśli narzędzie irytuje w tak małym przypadku, nie będę mu ufał w bardziej złożonym pipeline'ie.

Przykład CPU, który doprecyzował projekt

Najlepszym testem dla terminalowego CLI do wykresów jest to, czy rozwiązuje prawdziwy problem w terminalu. Dla mnie jednym z najlepszych przykładów jest wizualizacja użycia CPU bez otwierania graficznego monitora.

Czasem jeszcze nie potrzebuję profilera. Chcę tylko szybkiej odpowiedzi na prostsze pytanie: czy maszyna jest spokojna, czy ma piki, czy trend idzie w górę, czy jest przyklejona wysoko?

Na macOS:

while true; do top -l 1 | awk -F'[, %]+' '/^CPU usage:/ {print $3+$6}' sleep 1 done | simple-ascii-chart --stream --window 60 --height 10 --yRange 0 100 --title "CPU usage %"

Na Linuksie:

vmstat 1 | awk 'NR>2 {print 100-$15}' \ | simple-ascii-chart --stream --window 60 --height 10 --yRange 0 100 --title "CPU usage %"

Lubię ten przykład, bo od razu pokazuje sens CLI. To nie jest narzędzie do produkowania pięknych wykresów. Chodzi o zmniejszenie tarcia. Zamiast patrzeć, jak przewijają się liczby, widzę kształt sygnału. To często wystarcza, żeby odpowiedzieć na pierwsze pytanie i zdecydować, czy warto schodzić głębiej.

Ten przykład wymusił też projekt w użyteczny sposób:

  • strumieniowe wejście ze stdin
  • ruchome okno ostatnich wartości
  • ograniczanie częstotliwości przerysowań
  • stabilne limity osi Y dla danych procentowych
  • proste parsowanie liczb bez zbędnej ceremonii

Gdy narzędzie ma przetrwać prawdziwy one-liner, dekoracyjna złożoność bardzo szybko wychodzi na jaw.

Do czego wykresy terminalowe są dobre, a do czego nie

Uważam, że simple-ascii-chart najlepiej sprawdza się w sytuacjach, w których liczą się szybkość i bezpośredniość bardziej niż forma prezentacji:

  • szybka diagnostyka
  • wyjście ze skryptów lub CI
  • sesje SSH
  • lekkie monitorowanie
  • narzędzia dla programistów
  • dema i przykłady edukacyjne

To nie jest zamiennik dla grafiki prezentacyjnej ani gęstych wykresów analitycznych. Wykresy terminalowe mają twarde limity: mała szerokość wymusza kompromisy, gęste dane potrafią szybko stracić czytelność, a pewne pomysły wizualne po prostu nie tłumaczą się na czysty tekst.

Nie widzę tych ograniczeń jako porażek. Widzę je jako część szacunku dla medium.

Czego się nauczyłem

Budowanie tego projektu kilka rzeczy mocno mi rozjaśniło:

  • narzędzia terminalowe nagradzają powściągliwość
  • domyślne ustawienia są decyzjami produktowymi, nie detalami implementacyjnymi
  • UX w CLI ma znaczenie równie duże jak projekt API w bibliotece
  • renderowanie wyłącznie tekstowe wymusza jaśniejsze myślenie
  • małe projekty są dobrym miejscem do testowania mocnych założeń na prawdziwych kompromisach

Małe projekty dają dość powierzchni, żeby zderzyć się z prawdziwymi decyzjami inżynierskimi, bez znikania w złożoności.

Zakończenie

simple-ascii-chart zaczęło się jako mały eksperyment biblioteczny, ale stało się dobrym przypomnieniem, że dobre narzędzia często polegają na zmniejszaniu tarcia, a nie na maksymalizowaniu liczby funkcji.

Zbudowałem je z myślą o workflow, w których terminal jest już właściwym miejscem: skryptach, diagnostyce, sesjach SSH, szybkim monitorowaniu i małych narzędziach wewnętrznych. Właśnie tam, moim zdaniem, pasuje najlepiej.

Biblioteka nauczyła mnie wiele o wartościach domyślnych, powierzchni API i ograniczeniach renderowania. CLI nauczyło mnie jeszcze więcej o praktycznym UX. Strona z dokumentacją sprawiła, że całość łatwiej było eksplorować i udostępniać.

To mój ulubiony typ projektu: na tyle mały, by zachować wyraźne założenia, i na tyle użyteczny, by chcieć dalej go używać.

Projekt możesz zobaczyć tutaj: