Back to Blog

D3.js - krok po kroku. Skalowanie.

17 Apr 2016

Dziś bierzemy na warsztat skalowanie grafik w D3. Ten koncept warto znać nie tylko przy wykorzystywaniu “gołego” D3, ale też bibliotek stworzonych na jego bazie, jak C3.js czy NVD3. Często, aby z nich skorzystać, trzeba wiedzieć, jak użyć niektórych bazowych funkcji D3 - tak jest właśnie w przypadku API związanego ze skalowaniem.

W drugiej części wyrysowałem pierwsze elementy wykresu. Daleko im było jednak do doskonałości: długości prostokątów w pikselach odpowiadały ilości projektów DSP w poszczególnych kategoriach.

Taki scenariusz w rzeczywistości rzadko się zdarza. Zazwyczaj najlepiej mieć kontrolę nad wymiarami wykresu tzn. mieć z góry określoną jego wysokość i szerokość. Dlatego, zamiast używać wartości w danych wprost, wykres trzeba będzie przeskalować tak, aby wypełnił pożądany obszar.

Można to oczywiście zrobić ręcznie i samemu przeliczać wartości w danych na piksele, szerokości i pozycje słupków, i tak dalej. Na szczęście D3 oferuje pełny wachlarz pomocniczych funkcji, które zrobią większość pracy związanej z tego typu obliczeniami.

Czym są skale

Skala w D3 jest po prostu funkcją w rozumieniu matematycznym, czyli taką, która mapuje wartość z dziedziny na pewną wartość wyjściową. W naszym przykładzie dziedziną będą ilości projektów, dla których na wyjściu chcemy otrzymać długość bara w pikselach. Najprostszym rodzajem jest skala liniowa, czyli taka, w której wyjście zmienia się proporcjonalnie względem dziedziny (y = mx + b). I taką też skalę trzeba będzie użyć w przypadku wykresu: jeśli projektów jest 2 razy więcej, to chcemy pokazać 2 razy dłuższy prostokąt, proste.

Skalę w D3 konstruuje się przez podanie zakresu danych i zakresu wyjścia:

var myScale = d3.scale
  .linear()
  .domain([0,10])
  .range([200,300])

console.log(myScale(4.45)) //prints 244.5

Na podstawie argumentów domain() i range() D3 obliczy odpowiednie parametry skali i zwróci gotową funkcję. Powyższe zakresy nie ograniczają w żaden sposób wartości, jakie możemy przekazać bądź otrzymać. Można podać również dane spoza zakresu:

console.log(myScale(-8.3)) //prints ~117

Aby teraz użyć skalowania na wykresie, trzeba znaleźć kategorię z największą liczbą projektów i zmapować ją na pożądaną wysokość wykresu:

var xScale =
    d3.scale.linear()
      .domain([0,d3.max(categories.map(function(x){return x.count}))])
      .range([0,400])  

d3.select('#container')
  .selectAll('rect')
  .data(categories)
  .enter()	  
  .append('rect')
  //...
  .attr('width',function(d){return xScale(d.count); })
  //...

Wykres rozciągnie się na szerokość 400 pikseli:

Skale dyskretne

Poza najczęściej używaną skalą liniową, D3 daje do dyspozycji również skale logarytmiczne, potęgowe itp. czyli takie, które są funkcją ciągłą, mogącą przyjąć dowolną wartość liczbową.

Ale to nie wszystko, bo w ofercie dostępne są również skale dyskretne. Różnica jest taka, że przyjmują one z góry określone wartości dziedziny i mapują je na inną wartość.

Przykład:

var myScale =
  d3.scale
    .ordinal()
    .domain(['A',  'B',  'C','D', 'E'])
    .range( ['SSS','SCC','C','SC','?'])

console.log(myScale('A')) //prints SSS
console.log(myScale('E')) //prints ?

Jak widać, zamiast zakresów, należy podać konkretne wartości, przy czym nie trzeba ograniczać się tylko do liczb (Przemyciłem małą łamigłówkę. Co powinno być dla E?:).

Do czego może przydać się taka skala? Przypuśćmy, że chcemy wyświetlić litery A, B, C, D, E w równej odległości od siebie w linii prostej, na długości 100 pikseli. Możemy zdefiniować np. taką skalę, która zwróci nam odpowiednią pozycję każdej z liter:

var myScale =
  d3.scale
    .ordinal()
    .domain(['A','B','C','D','E'])
    .range ([0,  25, 50, 75, 100])

console.log(myScale('D')) //prints 75

Tu znów jednak trzeba byłoby przeliczać pozycje ręcznie, a przecież nie o to chodzi. Na szczęście wystarczy prosta zmiana:

var myScale =
  d3.scale
    .ordinal()
    .domain     (['A','B','C','D','E'])
    .rangePoints([0,100])

console.log(myScale('D')) //prints 75

Zamiast podawać kolejne pozycje ręcznie w range(), wystarczy użyć funkcji rangePoints() podając jedynie zakres, a ona załatwi już odpowiednie obliczenia.

Powiedzmy teraz, że chcielibyśmy zamiast równo ułożonych liter wyrysować np. prostokąty o równej szerokości (rangeBand) i pewnej proporcjonalnej odległości od siebie (padding). Na przykład tak jak na poniższym rysunku (zapożyczonym z dokumentacji D3):

Nic prostszego! Wystarczy użyć rangeBands():

var myScale =
  d3.scale
    .ordinal()
    .domain(['A','B','C','D','E'])
    //[0,100] is range interval
    //.2 is padding
    //and .1 is outerPadding.    
    .rangeBands( [0,100], .2, .1)

console.log(myScale('B')) //prints 22
console.log(myScale.rangeBand()) //prints 16
//outerpaddings, range bands and paddings gives
//2 + 16 + 4 + 16 + 4 + 16 + 4 + 16 + 4 + 16 + 2 = 100

myScale('B') zwróci pozycję, a myScale.rangeBand() szerokość prostokątów.

My w naszym przykładzie użyjemy tego sposobu do wyrysowania pozycji i szerokości prostokątów na wykresie:

//...
var yScale =
  d3.scale.ordinal()
    .domain(categories.map(function(d){return d.category}))
    .rangeRoundBands([0,400], 0.2) //optional outerPadding is omitted

d3.select('#container')
  .selectAll('rect')
  .data(categories)
  .enter()
  .append('rect')    
  .attr('y',function(d){return yScale(d.category)})
  .attr('height',yScale.rangeBand())
  //...

rangeRoundBands() działa dokładnie tak samo, jak rangeBands() z tą różnicą, że zwraca zaokrąglone wartości. Po tej modyfikacji wykres będzie rozciągnięty na wysokość 400 pikseli:

Wygląda to teraz znacznie lepiej oraz mamy kontrolę nad layoutem wykresu.

W następnym poście opiszę jak dodać etykiety i osie, czyli ostatni element potrzebny do tego, aby grafika stała się pełnoprawnym wykresem.