W poprzednim wpisie narysowaliśmy prosty wykres słupkowy przy użyciu D3.
W tamtym przykładzie nie użyliśmy SVG. Można narysować prostokąty przy użyciu elementów div. Sprawa jednak się komplikuje, gdy chcemy narysować nawet proste linie.
Nie ma co ukrywać, że w takim wypadku lepiej zabrać do swojego arsenału SVG.
SVG pozwala na rysowanie przeróżnych figur i elementów, które normalnie w HTML nie są łatwe do narysowania.
Używając D3, chcemy rysować wykresy, a nie przyciski, więc wydaje się to naturalne, że chcemy skorzystać z SVG.
Zanim zaczniemy rysować wykres w SVG, najpierw spróbujmy narysować coś prostszego, by zrozumieć jak SVG działa ze skryptem D3.js.
Najpierw nauczmy się jak tworzyć element SVG, a w nim umieszczać grupy elementów. Zobaczmy też jak do tych grup dodać odpowiednie transformacje SVG oraz efekty przejścia.
Tworzenie elementu SVG
Zacznijmy od tagu "<div>", który będzie kontenerem wizualizacyjnym. Używając funkcji append, z D3 dodamy do niego element SVG.
Później do tego elementu SVG dodamy odpowiednie atrybuty jak wysokość i szerokość obszaru SVG.
Na końcu do SVG dodamy kolejny element "circle", który określa proste kółko. Analogicznie do niego możemy dodać kolejne atrybuty.
<body>
<div id="circle"></div>
<script type="text/javascript">
var svg = d3.select('#circle')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('circle')
.style('stroke', 'black')
.style('fill', 'azure')
.attr('r', 40)
.attr('cx', 50)
.attr('cy', 50);
</script>
</body>
Oto efekt:
Zasady tworzenia elementów są więc dosyć proste. W podobny sposób mogę utworzyć kwadrat i umieścić je odpowiednio na osi X i Y kontenera SVG.
<div id="square"></div>
<script type="text/javascript">
var svg = d3.select('#square')
.append('svg')
.attr('width', 200)
.attr('height', 200);
svg.append('rect')
.style('stroke', 'blue')
.style('fill', 'yellow')
.attr('x', 60)
.attr('y', 60)
.attr('width', 70)
.attr('height', 70);
</script>
Kwadracik wygląda więc tak:
Transformacje SVG
Kluczowym aspektem SVG jest możliwość transformacji elementów. Mam do dyspozycji wiele transformacji, ja pokażę tylko 3 z nich:
- Skalowanie (Scale)
- Przesuniecie (Translate)
- Obracanie (Rotate)
Jest ich dużo więcej. Oto jak ich użyć na elemencie.
<div id="square" style="border: solid black 1px;"></div>
<script type="text/javascript">
var svg = d3.select('#square')
.append('svg')
.attr('width', 200)
.attr('height', 200);
var g = svg.append("svg:g");
g.append('rect').style('stroke', 'blue')
.style('fill', 'yellow')
.attr('x', 60)
.attr('y', 60)
.attr('width', 70)
.attr('height', 70)
.attr("transform", "translate(-30, 0),scale(2, 1),rotate(45, 75, 75)");
</script>
Kwadrat jest przesunięty, powiększony i obrócony.
Efekty przejścia
Zobaczyłeś już, jak używając D3, jesteśmy w stanie dodawać atrybuty do elementów SVG.
Używając D3, jesteśmy też w stanie zaanimować figury SVG. D3 oferuje 3 funkcje spełniające ten cel:
- transition()
- delay
- duration()
Naturalnie te funkcje dodaje się do elementów SVG. D3 potrafi rozpoznać każde wartości i je z interpolować.
<div id="circle"></div>
<script>
var svg = d3.select('#circle')
.append('svg')
.attr('width', 500)
.attr('height', 500);
svg.append('circle')
.style('stroke', 'blue')
.style('fill', 'blue')
.attr('r', 80)
.attr('cx', 80)
.attr('cy', 80)
.transition()
.delay(100)
.duration(12000)
.attr("r", 10)
.attr("cx", 150)
.style("fill", "red");
</script>
Powyżej deklaruję koło. Korzystając z trzech funkcji D3, mówię, że po 12 sekundach kółko:
- powinno mieć średnice 10,
- przesunąć się na osi X
- wypełnić się kolorem czerwonym
Cała animacja jest opóźniona o 100 milisekund. Jej rezultat jest widoczny na obrazku powyżej.
SVG w połączeniu ze skryptem D3 daje więc wiele możliwości, przejdźmy jednak do wykresu liniowego.
Wykres liniowy
Zanim zaczniemy działać, musimy mieć jakieś dane, do naszego wykresu. Oto więc nasza seria danych w postaci tablic JavaScript.
var data = [0, 30, 40, 30, 80, 75, 60, 50, 66, 77, 90, 60, 0];
Na początku też musimy ustalić przestrzeń, na jakiej wyświetli się nasz wykres. Określone są one tutaj zmiennymi w i h.
w = 400; h = 300;
Sam wykres powinien być trochę oddalony od swojego okna SVG. Zadeklarujmy więc też marginesy X I Y.
margin_x = 32; margin_y = 20;
Skala, zakres i przedziały
Na każdym wykresie liniowym z osią X i Y występuje skala oraz przedziały wartości.
Rozmieszczenie wartości na wykresie liniowy jest oczywiście zależne od skali i przedziałów wartości na danym wykresie liniowym.
Czym jest tak naprawdę skala.
Skala jest funkcją, która konwertuje wszystkie wartości w okręgu danego przedziału. Chcemy by jedna wartość, została umieszczona względem innej wartości.
Aby cały zbiór wyglądał, przejrzyście, na całym wykresie liniowym musimy ustalić jakąś skalę wartości, która będzie najbardziej sprawiedliwa dla całego zbioru.
Chcemy, aby nasz wykres był czytelny. Przedziały wartości muszą być więc w odpowiednich odstępach. Odpowiednie odstęp to właśnie skala.
Na rysunku widzimy, że przedziały wartości są w takiej samej długości. Na jednym wykresie jednak skala jest 1 do 1. Na drugim 1 do 20.
Przykładowo więc mógłbym powiedzieć, że na drugim wykresie 1 cm przedziału oznacza skok wartości o 20 jednostek.
Jak więc ustalić tę skalę. Nie musimy się zbytnio na tym wszystkim znać. D3 ma wbudowane funkcje do liczenia tych przedziałów.
var scale = d3.scale.linear().domain([0,35]).range([0,35]);
console.log(Math.round(scale(1)));
Co ten kod obrazuje. Powiedzmy, że mamy zbiór wartości. Wiemy, że najniższa wartość w tym zbiorze to zero. Największa wartość to 35.
domain([0,35])
Musimy więc ustalić domenę tego zbioru.
Domena to właśnie funkcja konwertująca wartości w odpowiedni sposób, by je umieścić na wykresie liniowym.
Deklaracja tej funkcji nie kończy się tutaj. Trzeb jednak jeszcze ustalić zakres naszego wykresu liniowego. Ustaliłem, że zakres będzie od 0 do 35. Taki sam jak zakres danych.
Zakres nie może mieć mniejszego przedziału niż największa i najmniejsza wartość zbioru danych. Wynika z tego, że nie możemy mieć ułamków w pikselach. Piksele to liczby całkowite.
Czym jest tak naprawdę zakres.
Powiedzmy, że w naszym zeszycie, w którym rysujemy wykres mamy do dyspozycji tylko 35 cm. Możemy więc narysować wartości od 0 do 35 po 1 centymetrze.
Jeśli na naszej kartce papieru mamy do dyspozycji 70 centymetrów oznacza to, że możemy rozrysować wartości od 0 do 35 po 2 centymetry.
.range([0,35]);
Ponieważ zakres i zbiór danych są takie same, nie powinno więc dziwić, że dane będą trafiać idealnie w swoje przedziały na osi.
Zmieniając jednak przedział oraz zakres maksymalny i minimalny danych mogę zobaczyć, że będą one trochę inaczej trafiać do osi. Ich skala będzie rosnąć za każdym razem, gdy zakres danych będzie mniejszy niż przedział, który podałem.
Tak, więc jeśli zakres jest od 0 do 15, a mój zeszyt HTML pozwala mi narysować od 0 px do 40px , to wartość 5 powinna być na 13px.
Jeśli zakres jest od 0 do 5, a mój zeszyt HTML pozwala mi narysować od 0px do 40 px, to wartość 5 powinna być na 40 pikselu.
Wewnątrz kodu
W praktyce wygląda to tak. Nasz zakres to wielkość ramki SVG, aby wykres wyglądał ładnie, dodajemy lub odejmujemy od tych wartości nasze marginesy.
Korzystając z kolejnych gotowych funkcji w D3, wyliczamy ze zbioru naszych danych najmniejsze i największe wartości.
Oś Y to maksymalna wartość w naszym zbiorze. Oś X to liczba elementów w naszej tablicy danych.
w = 400;
h = 300;
margin_x = 32;
margin_y = 20;
var data = [0, 30, 40, 30, 80, 75, 60, 50, 66, 77, 90, 60, 0];
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);
Mając to za sobą, możemy w końcu narysować jakiś wykres.
Najpierw dodajemy element SVG o wielkości i szerokości określonych w parametrach w i h.
w = 500;
h = 400;
margin_x = 32;
margin_y = 20;
var data = [0, 30, 40, 30, 80, 75, 60, 50, 66, 77, 90, 60, 0];
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var g = svg.append("svg:g")
.attr("transform", "translate(0," + h + ")");
var line = d3.svg.line()
.x(function (d, i) { return x(i); })
.y(function (d) { return -1 * y(d); });
g.append("svg:path").attr("d", line(data));
Wewnątrz elementu SVG dodajemy element g, który tak naprawdę określa grupę elementów.
Grupę tych elementów przesuniemy o liczbę pikseli określonych w elemencie h.
Normalnie, aby utworzyć linie SVG, musielibyśmy pisać jej deklarację od początku do końca. Robiliśmy tak wcześniej z kółkiem i kwadratem.
Na szczęście D3 posiada pole SVG, a w nim znajdują się odpowiednie funkcje do tworzenia figury SVG, w tym także linii.
var line = d3.svg.line()
.x(function(d, i) { return x(i); })
.y(function(d) { return -1 * y(d); });
g.append("svg:path").attr("d", line(data));
Linia posiada pola X i Y, które przyjmują funkcje na bazie, której będziemy wyliczać ich miejsca na osi X oraz na osi Y.
Funkcja osi Y jest mnożona przez –1. Dlaczego? Jak wcześniej pamiętasz, przesunęliśmy cały wykres o jego kompletną wysokość. Im więc wartość ujemna większa, tym wyżej element będzie na wykresie.
Jeśli uruchomisz, tę funkcję zobaczysz następujący wykres.
Coś tu jest chyba nie tak. Oczywiście brakuje tutaj jakiś styli. Elementy SVG mogą być o stylowane przy użyciu CSS.
<style>path {
stroke: red;
stroke-width: 2;
fill: none;
}
line {
stroke: black;
}
</style>
Po dodaniu tych styli wykres wygląda już bardziej przyjaźnie.
Wciąż jednak brakuje mu osi X i Y.
Dodajmy więc kolejne linie symbolizującą naszą oś.
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0));
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(d3.max(data)) - 10);
Teraz możemy nazwać to wykresem.
Brakuje jednak jeszcze skali. Dla ułatwienia zadania D3 ma funkcję "ticks()".
Funkcja ta skaluje wartość X i Y i zwraca listę okresów do naszego wykresu.
Funkcja text(String) zabiera wartość tekstową obecnego elementu.
//draw the xLabels
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function (d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle");
// draw the yLabels
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function (d) { return -y(d) })
.attr("text-anchor", "end");
Aby ułożyć element tekstowy ("label") w odpowiedni sposób możemy skorzystać z atrybutu "text-anchor"
Możemy też tę właściwość ustawić też po stronie CSS.
<style>
path {
stroke: red;
stroke-width: 2;
fill: none;
}
line {
stroke: black;
}
.xLabel {
text-anchor: middle;
}
.yLabel {
text-anchor: end;
}
</style>
Nasz wykres posiada więc już listę zakresów po osi X i Y.
W wykresie jeszcze brakuje małych kresek pod wartościami na osiach X i Y.
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function (d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function (d) { return x(d); })
.attr("y2", -y(0) - 5)
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function (d) { return -y(d); })
.attr("x1", x(0) + 5)
.attr("y2", function (d) { return -y(d); })
.attr("x2", x(0))
Dodajemy je w podobny sposób.
To jednak jeszcze nie koniec. Do tego wykresu trzeba dodać jeszcze wiele innych rzeczy.
Sam wykres, aby był bardziej czytelny, musi jeszcze zawierać w sobie siatkę. Sama siatka dobrze, aby była bladego koloru.
.xGrids {
stroke: lightgray;
}
.yGrids {
stroke: lightgray;
}
Oto kod rysujący siatkę.
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function (d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function (d) { return x(d); })
.attr("y2", -y(d3.max(data)) - 10);
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function (d) { return -y(d); })
.attr("x1", x(w))
.attr("y2", function (d) { return -y(d); })
.attr("x2", x(0));
Jeśli się dobrze przyjrzysz temu rysunkowi, zobaczysz pewien problem.
Pozwól, że powiększę ten obrazek, aby można było problem zauważyć lepiej.
Siatka jest obecnie rysowana na końcu, a to znaczy, że jest ona na wierzchu i zakrywa linię wykresu.
Poprawmy więc kolejność wykonywania działań. Najpierw narysujmy siatkę, a dopiero później linie wykresu.
w = 500;
h = 400;
margin_x = 32;
margin_y = 20;
var data = [0, 30, 40, 30, 80, 75, 60, 50, 66, 77, 90, 60, 0];
y = d3.scale.linear().domain([0, d3.max(data)]).range([0 + margin_y, h - margin_y]);
x = d3.scale.linear().domain([0, data.length]).range([0 + margin_x, w - margin_x]);
var svg = d3.select("body")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
var g = svg.append("svg:g")
.attr("transform", "translate(0," + h + ")");
// draw the x axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(w))
.attr("y2", -y(0));
// draw the y axis
g.append("svg:line")
.attr("x1", x(0))
.attr("y1", -y(0))
.attr("x2", x(0))
.attr("y2", -y(d3.max(data)) - 10);
//draw the xLabels
g.selectAll(".xLabel")
.data(x.ticks(5))
.enter().append("svg:text")
.attr("class", "xLabel")
.text(String)
.attr("x", function (d) { return x(d) })
.attr("y", 0)
.attr("text-anchor", "middle");
// draw the yLabels
g.selectAll(".yLabel")
.data(y.ticks(5))
.enter().append("svg:text")
.attr("class", "yLabel")
.text(String)
.attr("x", 25)
.attr("y", function (d) { return -y(d) })
.attr("text-anchor", "end");
//draw the x ticks
g.selectAll(".xTicks")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xTicks")
.attr("x1", function (d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function (d) { return x(d); })
.attr("y2", -y(0) - 5)
// draw the y ticks
g.selectAll(".yTicks")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yTicks")
.attr("y1", function (d) { return -y(d); })
.attr("x1", x(0) + 5)
.attr("y2", function (d) { return -y(d); })
.attr("x2", x(0))
//draw the x grid
g.selectAll(".xGrids")
.data(x.ticks(5))
.enter().append("svg:line")
.attr("class", "xGrids")
.attr("x1", function (d) { return x(d); })
.attr("y1", -y(0))
.attr("x2", function (d) { return x(d); })
.attr("y2", -y(d3.max(data)) - 10);
// draw the y grid
g.selectAll(".yGrids")
.data(y.ticks(5))
.enter().append("svg:line")
.attr("class", "yGrids")
.attr("y1", function (d) { return -y(d); })
.attr("x1", x(w))
.attr("y2", function (d) { return -y(d); })
.attr("x2", x(0));
var line = d3.svg.line()
.x(function (d, i) { return x(i); })
.y(function (d) { return -1 * y(d); });
g.append("svg:path").attr("d", line(data));
Teraz kolejność rysowania jest poprawna.
Zabawa z wykresem liniowym i D3 nie kończy się jednak tutaj.