W kierunku aktualizowalnych wykresów D3.js

Opublikowany: 2022-03-11

Wstęp

D3.js to biblioteka open source do wizualizacji danych opracowana przez Mike'a Bostocka. D3 oznacza dokumenty oparte na danych, a jak sama nazwa wskazuje, biblioteka pozwala programistom na łatwe generowanie i manipulowanie elementami DOM na podstawie danych. Chociaż nie jest ograniczony możliwościami biblioteki, D3.js jest zwykle używany z elementami SVG i oferuje potężne narzędzia do tworzenia od podstaw wizualizacji danych wektorowych.

Aktualizowany wzór wykresu pozwala na łatwe tworzenie wykresów D3.js
Ćwierkać

Zacznijmy od prostego przykładu. Załóżmy, że trenujesz do biegu na 5 km i chcesz sporządzić poziomy wykres słupkowy przedstawiający liczbę przejechanych mil każdego dnia ostatniego tygodnia:

 var milesRun = [2, 5, 4, 1, 2, 6, 5]; d3.select('body').append('svg') .attr('height', 300) .attr('width', 800) .selectAll('rect') .data(milesRun) .enter() .append('rect') .attr('y', function (d, i) { return i * 40 }) .attr('height', 35) .attr('x', 0) .attr('width', function (d) { return d*100}) .style('fill', 'steelblue');

Aby zobaczyć to w akcji, sprawdź to na bl.ocks.org.

Jeśli ten kod wygląda znajomo, to świetnie. Jeśli nie, uważam, że samouczki Scotta Murraya są doskonałym źródłem do rozpoczęcia pracy z D3.js.

Jako freelancer, który przepracował setki godzin nad rozwojem w D3.js, mój wzorzec rozwoju przeszedł ewolucję, zawsze mając na celu stworzenie najbardziej wszechstronnych doświadczeń klientów i użytkowników. Jak omówię bardziej szczegółowo później, wzorzec Mike'a Bostocka dla wykresów wielokrotnego użytku oferuje wypróbowaną i prawdziwą metodę implementacji tego samego wykresu w dowolnej liczbie selekcji. Jednak jego ograniczenia są realizowane po zainicjowaniu wykresu. Jeśli chciałem wykorzystać przejścia i wzorce aktualizacji D3 za pomocą tej metody, zmiany danych musiały być obsługiwane w całości w tym samym zakresie, w jakim został wygenerowany wykres. W praktyce oznaczało to implementację filtrów, wyborów rozwijanych, suwaków i opcji zmiany rozmiaru w ramach tego samego zakresu funkcji.

Po wielokrotnym doświadczaniu tych ograniczeń na własnej skórze, chciałem stworzyć sposób na wykorzystanie pełnej mocy D3.js. Na przykład nasłuchiwanie zmian na liście rozwijanej całkowicie oddzielnego komponentu i płynne wyzwalanie aktualizacji wykresów ze starych danych na nowe. Chciałem móc przekazać kontrolki wykresów z pełną funkcjonalnością i zrobić to w sposób logiczny i modułowy. Rezultatem jest formacja wykresu, którą można aktualizować, a ja zamierzam przejść przez cały proces tworzenia tego wzoru.

Wykresy D3.js Progresja wzoru

Krok 1: Zmienne konfiguracyjne

Kiedy zacząłem używać D3.js do tworzenia wizualizacji, bardzo wygodne stało się używanie zmiennych konfiguracyjnych do szybkiego definiowania i zmieniania specyfikacji wykresu. Dzięki temu moje wykresy mogły obsługiwać różne długości i wartości danych. Ten sam fragment kodu, który wyświetlał przebiegi mil, mógł teraz wyświetlać dłuższą listę temperatur bez żadnych problemów:

 var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68]; var height = 300; var width = 800; var barPadding = 1; var barSpacing = height / highTemperatures.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(highTemperatures); var widthScale = width / maxValue; d3.select('body').append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(highTemperatures) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', 'steelblue');

Aby zobaczyć to w akcji, sprawdź to na bl.ocks.org.

Zwróć uwagę, jak wysokości i szerokości pasków są skalowane na podstawie zarówno rozmiaru, jak i wartości danych. Jedna zmienna jest zmieniana, a reszta jest załatwiana.

Krok 2: Łatwe powtarzanie za pomocą funkcji

Kodowanie nigdy nie powinno być ćwiczeniem polegającym na kopiowaniu i wklejaniu

Kodowanie nigdy nie powinno być ćwiczeniem polegającym na kopiowaniu i wklejaniu
Ćwierkać

Wyodrębniając część logiki biznesowej, jesteśmy w stanie stworzyć bardziej wszechstronny kod, który jest gotowy do obsługi uogólnionego szablonu danych. Następnym krokiem jest zawinięcie tego kodu w funkcję generowania, która redukuje inicjalizację do tylko jednego wiersza. Funkcja przyjmuje trzy argumenty: dane, obiekt docelowy DOM i obiekt opcji, których można użyć do nadpisania domyślnych zmiennych konfiguracyjnych. Zobacz, jak można to zrobić:

 var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; function drawChart(dom, data, options) { var width = options.width || 800; var height = options.height || 200; var barPadding = options.barPadding || 1; var fillColor = options.fillColor || 'steelblue'; var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(dom).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); } var weatherOptions = {fillColor: 'coral'}; drawChart('#weatherHistory', highTemperatures, weatherOptions); var runningOptions = {barPadding: 2}; drawChart('#runningHistory', milesRun, runningOptions);

Aby zobaczyć to w akcji, sprawdź to na bl.ocks.org.

W tym kontekście ważne jest również, aby zanotować wybór D3.js. Należy zawsze unikać ogólnych wyborów, takich jak d3.selectAll('rect') . Jeśli pliki SVG są obecne w innym miejscu na stronie, wszystkie rect na stronie stają się częścią zaznaczenia. Zamiast tego, korzystając z przekazanego odwołania DOM, utwórz jeden obiekt svg , do którego możesz się odwoływać podczas dołączania i aktualizowania elementów. Ta technika może również poprawić czas wykonywania generowania wykresu, ponieważ użycie odniesienia, takiego jak słupki, zapobiega również konieczności ponownego wyboru D3.js.

Krok 3: Łączenie metod i selekcje

Podczas gdy poprzedni szkielet wykorzystujący obiekty konfiguracyjne jest bardzo powszechny w bibliotekach JavaScript, Mike Bostock, twórca D3.js, zaleca inny wzorzec do tworzenia wykresów wielokrotnego użytku. Krótko mówiąc, Mike Bostock zaleca implementację wykresów jako zamknięć za pomocą metod getter-setter. Dodając nieco złożoności implementacji wykresu, ustawianie opcji konfiguracyjnych staje się bardzo proste dla wywołującego, po prostu za pomocą łączenia metod:

 // Using Mike Bostock's Towards Reusable Charts Pattern function barChart() { // All options that should be accessible to caller var width = 900; var height = 200; var barPadding = 1; var fillColor = 'steelblue'; function chart(selection){ selection.each(function (data) { var barSpacing = height / data.length; var barHeight = barSpacing - barPadding; var maxValue = d3.max(data); var widthScale = width / maxValue; d3.select(this).append('svg') .attr('height', height) .attr('width', width) .selectAll('rect') .data(data) .enter() .append('rect') .attr('y', function (d, i) { return i * barSpacing }) .attr('height', barHeight) .attr('x', 0) .attr('width', function (d) { return d*widthScale}) .style('fill', fillColor); }); } chart.width = function(value) { if (!arguments.length) return margin; width = value; return chart; }; chart.height = function(value) { if (!arguments.length) return height; height = value; return chart; }; chart.barPadding = function(value) { if (!arguments.length) return barPadding; barPadding = value; return chart; }; chart.fillColor = function(value) { if (!arguments.length) return fillColor; fillColor = value; return chart; }; return chart; } var milesRun = [2, 5, 4, 1, 2, 6, 5]; var highTemperatures = [77, 71, 82, 87, 84, 78, 80, 84, 86, 72, 71, 68, 75, 73, 80, 85, 86, 80]; var runningChart = barChart().barPadding(2); d3.select('#runningHistory') .datum(milesRun) .call(runningChart); var weatherChart = barChart().fillColor('coral'); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

Aby zobaczyć to w akcji, sprawdź to na bl.ocks.org.

Inicjalizacja wykresu wykorzystuje selekcję D3.js, wiążąc odpowiednie dane i przekazując selekcję DOM jako this kontekst do funkcji generatora. Funkcja generatora otacza domyślne zmienne w zamknięciu i umożliwia wywołującemu zmianę ich poprzez łączenie metod z funkcjami konfiguracyjnymi, które zwracają obiekt wykresu. W ten sposób osoba wywołująca może renderować ten sam wykres do wielu zaznaczeń naraz lub użyć jednego wykresu do renderowania tego samego wykresu do różnych zaznaczeń z różnymi danymi, unikając przy tym przekazywania nieporęcznego obiektu opcji.

Krok 4: Nowy wzorzec dla aktualizowanych wykresów

Poprzedni wzór sugerowany przez Mike'a Bostocka daje nam, jako twórcom wykresów, dużą moc w funkcji generatora. Mając jeden zestaw danych i wszelkie przekazane konfiguracje połączone w łańcuch, kontrolujemy wszystko stamtąd. Jeśli dane muszą zostać zmienione od wewnątrz, możemy użyć odpowiednich przejść zamiast tylko przerysowywać od zera. Nawet takie rzeczy, jak zmiana rozmiaru okna, można elegancko obsługiwać, tworząc responsywne funkcje, takie jak używanie skróconego tekstu lub zmiana etykiet osi.

Ale co, jeśli dane zostaną zmodyfikowane spoza zakresu funkcji generatora? A co, jeśli rozmiar wykresu wymaga programowej zmiany rozmiaru? Moglibyśmy po prostu ponownie wywołać funkcję wykresu, z nowymi danymi i nową konfiguracją rozmiaru. Wszystko zostanie narysowane od nowa i voila. Problem rozwiązany.

Niestety z tym rozwiązaniem wiąże się szereg problemów.

Po pierwsze, prawie nieuchronnie wykonujemy niepotrzebne obliczenia inicjalizacji. Po co skomplikowana manipulacja danymi, skoro wszystko, co musimy zrobić, to skalować szerokość? Te obliczenia mogą być konieczne przy pierwszym inicjalizacji wykresu, ale z pewnością nie przy każdej aktualizacji, którą musimy wykonać. Każde żądanie programistyczne wymaga pewnej modyfikacji, a jako programiści dokładnie wiemy, czym są te zmiany. Nie więcej nie mniej. Co więcej, w zakresie wykresu mamy już dostęp do wielu rzeczy, których potrzebujemy (obiekty SVG, aktualne stany danych i inne), dzięki czemu zmiany są proste do wprowadzenia.

Weźmy na przykład powyższy przykład wykresu słupkowego. Gdybyśmy chcieli zaktualizować szerokość, a zrobiliśmy to poprzez przerysowanie całego wykresu, uruchomilibyśmy wiele niepotrzebnych obliczeń: znalezienie maksymalnej wartości danych, obliczenie wysokości słupka i renderowanie wszystkich tych elementów SVG. Naprawdę, gdy width zostanie przypisana do nowej wartości, jedyne zmiany, które musimy wprowadzić, to:

 width = newWidth; widthScale = width / maxValue; bars.attr('width', function(d) { return d*widthScale}); svg.attr('width', width);

Ale jest jeszcze lepiej. Ponieważ mamy teraz pewną historię wykresu, możemy użyć wbudowanych przejść D3, aby zaktualizować nasze wykresy i łatwo je animować. Kontynuując powyższy przykład, dodanie przejścia na width jest tak proste, jak zmiana

 bars.attr('width', function(d) { return d*widthScale});

do

 bars.transition().duration(1000).attr('width', function(d) { return d*widthScale});

Co więcej, jeśli pozwolimy użytkownikowi przekazać nowy zestaw danych, możemy użyć opcji aktualizacji D3 (enter, update i exit), aby również zastosować przejścia do nowych danych. Ale jak pozwalamy na nowe dane? Jeśli pamiętasz, nasza poprzednia implementacja stworzyła nowy wykres, taki jak ten:

 d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

Powiązaliśmy dane z zaznaczeniem D3.js i nazwaliśmy nasz wykres wielokrotnego użytku. Wszelkie zmiany danych musiałyby być dokonywane poprzez powiązanie nowych danych z tym samym wyborem. Teoretycznie moglibyśmy użyć starego wzorca i zbadać selekcję pod kątem istniejących danych, a następnie zaktualizować nasze ustalenia o nowe dane. Jest to nie tylko kłopotliwe i skomplikowane do wdrożenia, ale wymagałoby założenia, że ​​istniejący wykres był tego samego typu i formy.

Zamiast tego, po wprowadzeniu pewnych zmian w strukturze funkcji generatora JavaScript, możemy stworzyć wykres, który pozwoli wywołującemu łatwo wywołać zmiany na zewnątrz poprzez łączenie metod. Podczas gdy przed ustawieniem konfiguracji i danych, a następnie pozostawieniem ich bez zmian, dzwoniący może teraz zrobić coś takiego, nawet po zainicjowaniu wykresu:

 weatherChart.width(420);

Rezultatem jest płynne przejście do nowej szerokości z istniejącego wykresu. Bez zbędnych obliczeń i z eleganckimi przejściami, rezultatem jest zadowolony klient.

Bez zbędnych obliczeń + eleganckie przejścia = zadowolony klient

Bez zbędnych obliczeń + eleganckie przejścia = zadowolony klient
Ćwierkać

Ta dodatkowa funkcjonalność wiąże się z niewielkim wzrostem wysiłku programistów. Jest to jednak wysiłek, który z historycznego punktu widzenia okazał się wart tego czasu. Oto szkielet wykresu, który można aktualizować:

 function barChart() { // All options that should be accessible to caller var data = []; var width = 800; //... the rest var updateData; var updateWidth; //... the rest function chart(selection){ selection.each(function () { // //draw the chart here using data, width // updateWidth = function() { // use width to make any changes }; updateData = function() { // use D3 update pattern with data } }); } chart.data = function(value) { if (!arguments.length) return data; data = value; if (typeof updateData === 'function') updateData(); return chart; }; chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; }; //... the rest return chart; }

Aby zobaczyć w pełni zaimplementowane, sprawdź to na block.ocks.org.

Przyjrzyjmy się nowej strukturze. Największą zmianą w stosunku do poprzedniej implementacji zamknięcia jest dodanie funkcji aktualizacji. Jak wspomniano wcześniej, funkcje te wykorzystują przejścia D3.js i wzorce aktualizacji, aby płynnie wprowadzać wszelkie niezbędne zmiany w oparciu o nowe dane lub konfiguracje wykresów. Aby były dostępne dla wywołującego, funkcje są dodawane jako właściwości do wykresu. Aby było jeszcze łatwiej, zarówno początkowa konfiguracja, jak i aktualizacje są obsługiwane za pomocą tej samej funkcji:

 chart.width = function(value) { if (!arguments.length) return width; width = value; if (typeof updateWidth === 'function') updateWidth(); return chart; };

Należy zauważyć, że updateWidth nie zostanie zdefiniowany, dopóki wykres nie zostanie zainicjowany. Jeśli jest undefined , to zmienna konfiguracyjna zostanie ustawiona globalnie i będzie używana w zamknięciu wykresu. Jeśli funkcja wykresu została wywołana, wszystkie przejścia są przekazywane do funkcji updateWidth , która używa zmiennej width zmienionej w celu wprowadzenia niezbędnych zmian. Coś takiego:

 updateWidth = function() { widthScale = width / maxValue; bars.transition().duration(1000).attr('width', function(d) { return d*widthScale}); svg.transition().duration(1000).attr('width', width); };

Dzięki tej nowej strukturze dane dla wykresu są przekazywane przez tworzenie łańcucha metod, tak jak każda inna zmienna konfiguracyjna, zamiast wiązania ich z wyborem D3.js. Różnica:

 var weatherChart = barChart(); d3.select('#weatherHistory') .datum(highTemperatures) .call(weatherChart);

co staje się:

 var weatherChart = barChart().data(highTemperatures); d3.select('#weatherHistory') .call(weatherChart);

Wprowadziliśmy więc pewne zmiany i dodaliśmy trochę wysiłku programistów, zobaczmy korzyści.

Załóżmy, że masz nowe żądanie funkcji: „Dodaj listę rozwijaną, aby użytkownik mógł przełączać się między wysokimi a niskimi temperaturami. I spraw, aby zmieniał się również kolor, kiedy jesteś przy tym. Zamiast czyścić aktualny wykres, wiązać nowe dane i przerysowywać od nowa, teraz możesz wykonać proste połączenie, gdy wybrana jest niska temperatura:

 weatherChart.data(lowTemperatures).fillColor('blue');

i ciesz się magią. Nie tylko oszczędzamy obliczenia, ale dodajemy nowy poziom zrozumienia do wizualizacji w miarę jej aktualizacji, co wcześniej nie było możliwe.

Potrzebne jest tutaj ważne słowo przestrogi dotyczące przejść. Zachowaj ostrożność podczas planowania wielu przejść na tym samym elemencie. Rozpoczęcie nowego przejścia spowoduje niejawne anulowanie wszystkich poprzednio uruchomionych przejść. Oczywiście w jednym przejściu zainicjowanym przez D3.js można zmienić wiele atrybutów lub stylów elementu, ale natknąłem się na kilka przypadków, w których wiele przejść jest wyzwalanych jednocześnie. W takich przypadkach rozważ użycie współbieżnych przejść w elementach nadrzędnych i podrzędnych podczas tworzenia funkcji aktualizacji.

Zmiana w filozofii

Mike Bostock wprowadza zamknięcia jako sposób na hermetyzację generowania wykresów. Jego wzór jest zoptymalizowany pod kątem tworzenia tego samego wykresu z różnymi danymi w wielu miejscach. Jednak przez lata pracy z D3.js zauważyłem niewielką różnicę w priorytetach. Zamiast używać jednej instancji wykresu do tworzenia tej samej wizualizacji z różnymi danymi, nowy wzorzec, który wprowadziłem, umożliwia rozmówcy łatwe tworzenie wielu instancji wykresu, z których każdy może być w pełni modyfikowany nawet po zainicjowaniu. Co więcej, każda z tych aktualizacji jest obsługiwana z pełnym dostępem do aktualnego stanu wykresu, co pozwala programiście wyeliminować niepotrzebne obliczenia i wykorzystać moc D3.js do tworzenia bardziej płynnych doświadczeń użytkowników i klientów.