Chart.js – wykresy

Tym razem weźmiemy pod warsztat tworzenie wykresów z wykorzystaniem biblioteki ChartJS. Jest darmowa biblioteka umożliwiająca umieścić na naszej stronie wykresy wszelakiego typu. Nie ma może możliwości jak profesjonalne biblioteki (jak chociażby Highcharts), ale w wielu miejscach w zupełności wystarcza.
W poniższym tekście nagniemy jej domyśle ustawienia tak by były bardziej uniwerstalne.

Podstawowa implementacja

Aby móc działać, ChartJS wymaga dodania do strony swojej biblioteki, oraz stworzenia canvasu, w którym będzie rysowany wykres:

Szybko dodajemy wspomniane biblioteki poleceniem

npm install chart.js jquery --save

I wrzucamy je do naprawdę prostegho HTML:

<!doctype html>
<html>
    <head>
        <title>Line Chart</title>
        <meta charset="utf-8">
        <script src="node_modules/chart.js/dist/Chart.bundle.min.js"></script>
        <script src="node_modules/jquery/dist/jquery.min.js"></script>
        <link rel="stylesheet" href="style.css">
    </head>

    <body>
        <div class="chart-cnt">
            <canvas id="myChart" width="600" height="200"></canvas>
        </div>
        <script type="text/javascript" src="script.js"></script>
    </body>
</html>

Całość będziemy pisać w oddzielnym pliku scripts.js.
Dla naszego przykładu weźmy przykładową konfigurację z dokumentacji chartJS:

let data = {
    labels: ["January", "February", "March", "April"],
    datasets: [
        {
            label: "My First dataset",
            //linia
            //borderDash: [3, 3], //jezeli ustawione to przerywana linia
            borderColor : 'rgba(236,115,87, 0.7)',
            pointBorderColor : 'rgba(236,115,87, 0.7)',
            borderWidth : 2,
            //kolor tla i legendy
            fill: true, //czy wypelnic zbior
            backgroundColor : 'rgba(236,115,87, 0.1)', //wplywa tez na kolor w legendzie
            //ustawienia punktu
            pointRadius : 4,
            pointBorderWidth : 1,
            pointBackgroundColor : 'rgba(255,255,255,1)',
            //ustawienia punktu hover
            pointHoverRadius: 4,
            pointHoverBorderWidth: 3,
            pointHoverBackgroundColor : 'rgba(255,255,255,1)',
            pointHoverBorderColor : 'rgba(236,115,87, 1)',
            data: [50, 30, 40, 30, 32, 25, 30],
        },
        {
            label: "My Second dataset",
            borderColor : 'rgba(75,192,192, 0.7)',
            ...
            data: [65, 59, 80, 81, 56, 55, 40],
        },
        {
            label: "My Third dataset",
            borderColor : 'rgba(132,177,237, 0.7)',
            ...
            data: [30, 20, 60, 50, 42, 15, 40],
        }
    ]
};

let options = {
};

let ctx = document.getElementById("myChart").getContext("2d");;
let myLineChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: options
});

Super prosty wykres

Upiększanie wyglądu wykresu

Wygląda to całkiem dobrze, ale zawsze można taki wykres dopracować.
Zacznijmy od poprawy otoczki wykresu czyli poprawy siatki i opisów osi.
ChartJS udostępnia całkiem spore możliwości konfiguracji.
Są logicznie nazwane, więc poniższy kod łatwo będzie zrozumieć:

let data = {
    ...
}

let options = {
    animation: {
        easing: "easeOutCubic",
        duration: 700
    },
    responsive: true, //responsywnosc
    legend: {
        position: 'bottom', //położenie legendy
        display: true //pokazuj legendę
    },
    hover: {
        mode: 'dataset' //jak pokazywać tooltipy po najechaniu na punkty wykresu
        //mode: 'label',
    },
    scales: {
        xAxes: [{ //linie x
            gridLines: {
                zeroLineWidth: 1, //linia x=0
                zeroLineColor: 'rgba(0,0,0,0.3)', //kolor lini x=0
                color: "rgba(0, 0, 0, 0.05)", //kolor linii
                lineWidth: 1 //szerokośc linii
            },
            display: true, //czy pokazywac dolne opisy jednostek
            scaleLabel: { //tytuł osi x
                display: true,
                labelString: 'Month',
                fontSize: 12,
                fontStyle: 'bold'
            },
            ticks: { //rozmiar jednostek
                fontSize: 10
            },
            labelFormatter: function(e) {
                return "x: " + e.value;
            }
        }],
        yAxes: [{ //linie y
            gridLines: {
                zeroLineWidth: 1,
                zeroLineColor: 'rgba(0,0,0,0.3)',
                color: "rgba(0, 0, 0, 0.05)",
                lineWidth: 1
            },
            display: true,
            scaleLabel: {
                display: true,
                labelString: 'Wartość',
                fontSize: 13,
                fontStyle: 'bold'
            },
            ticks: {
                fontSize: 12,
                min: 0,
                max: 100
            }
        }]
    },
    title: { //tytuł wykresu
        display: true,
        text: 'Przykładowy wykres'
    }
};

let ctx = document.getElementById("myChart");
let myLineChart = new Chart(ctx, {
    type: 'line',
    data: data,
    options: options
});

Poprawiony wygląd wykresu

Pewne założenia

I w zasadzie na tym moglibyśmy zakończyć. Wykres jest, do zobaczenia w następnym artykule.
Uff. To było łatwe.

Ja widzę tutaj jednak kilka spraw, którymi warto się zająć.
Po pierwsze powyższy kod bardzo szybko się nam rozrasta. Przy kilkunastu datasetach kod zająłby pewnie z kilka ekranów.
Dwa – sposób przekazywania danych.
Nie wydaje się wam, że dane w powyższej formie są przezywane trochę „na czuja”?
Powiedzmy, że z serwera pobieralibyśmy porcję danych. Dla chartJS dane takie musiały by wyglądać tak:

[
    {
        "name" : "My First dataset",
        "data" : [70, 65, 20, 10, 11]
    },
    {
        "name" : "My Second dataset",
        "data" : [54, 52, 51, 20, 10]
    },
    {
        "name" : "My Third dataset",
        "data" : [16, 36, 27, 30, 40]
    },
]

Niby te dane są ok – prawda?. Nie wiemy jednak czego się one tyczą. Co oznacza pierwsza „65” w pierwszym zbiorze?
Przy małych prostych wykresach które mają służyć wyświetleniu nieokreślonych „kresek” taka forma danych się sprawdzi.
Ale nie wydaje się ona optymalna. W praktyce dane takie będą do nas raczej trafiać w takiej formie:

[
    {
        "charName" : "Przykładowe dane",
        "data" : [
            {
                "name": "January",
                "values": {
                    "My First dataset": 70,
                    "My Second dataset": 54,
                    "My Third dataset": 16
                }
            },
            {
                "name": "February",
                "values": {
                    "My First dataset": 65,
                    "My Second dataset": 52,
                    "My Third dataset": 36
                }
            },
            {
                "name": "March",
                "values": {
                    "My First dataset": 20,
                    "My Second dataset": 51,
                    "My Third dataset": 27
                }
            },
            ...
        ]
    }
]

Dzięki temu dokładnie wiemy czego dane się tyczą i gdzie powinny znaleźć się na wykresie.
W końcu trzy – podając konfigurację w pierwotnej postaci, musimy dokładnie wiedzieć ile takich datasetów dostaniemy, bo dla każdego z nich musimy przekazać odpowiednią konfigurację z kolorami włącznie.
Czwarty punkt to już Usability. Bardzo fajnie, że twórcy w v2 ChartJS dali klikalne labele w legendzie. Niestety możliwość kliknięcia w taką legendę nie jest dostatecznie czytelna. Dodatkowo za pomocą
klawiatury nie jesteś w stanie wykonać takiego kliknięcia, a po najechaniu kursorem na legendę, nie przyjmuje on postaci łapki.

W wielu przypkach te cztery punkty nie będą przeszkodą, Ale czasami taką mogą być, dlatego w ramach treningu zajmiemy się
ich rozwiązaniem.

Pobieramy dane z serwera

Dane w „lepszej” postaci pobierzemy z serwera, a następnie zamienimy na format przeznaczony dla ChartJS.
Zastosujemy tutaj to samo podejście z $.Deffered(), które zastosowaliśmy przy tworzeniu Google Map:

$(function() {
    let myChart = null; //wykres

    let getData = function() {
        let deffer = $.Deferred();

        $.ajax({
            url : 'data.json',
            dataType: 'json',
            cache : false,
            success: function(ret) {
                return deffer.resolve(ret);
            },
            error : function(error) {
                return deffer.reject(error);
            }
        });

        return deffer;
    }

    let init = function() {
        $.when(getData()).then(function(ret) {
            ...
        }, function(err) {
            console.warn('Błąd pobierania danych: ', err);
        });
    }
    init();
})();


Jak działa sam obiekt $.Deferred opisałem w artykule o mapie. Tutaj to w zasadzie to samo.
Po pobraniu danych musimy je zamienić na format dla chartJS. Wiemy już, że format danych dla ChartJS powinien mieć ogólną postać:

type : 'line',
options : {...},
labels : [...],
data : {
    datasets : [
        {
            label : "nazwa zbioru danych",
            data : [...wartosci do wyswietlenia...]
        },
        {
            ...
        }
    ]
}

Opcje options wygenerujemy trochę później. Zajmijmy się pozostałymi danymi.
Pierwszą czynnością będzie zebranie nazw labels, które są jednostkami wyświetlanymi na osi X. To ta prostsza część.
Drugą jest konwersja naszych danych z serwera na obiekty, które przekażemy do tablicy datasets, a które będą zawierać dane i label oznaczający nazwę zbioru.

Jak dokonać takiej konwersji?

Zróbmy pętlę po kluczach pierwszego zbioru values. Stwórzmy tablicę, której kluczami będą pobrane z values klucze. Wartościami będą obiekty z uporządkowanymi danymi. Czyli w rezultacie otrzymamy:

temp["My First dataset"] = {}
temp["My Second dataset"] = {}
temp["My Third dataset"] = {}

Utwórzmy taką tablicę za pomocą kodu:

if (data.data.length) {
    for (let i in data.data[0].values) {
        if (data.data[0].values.hasOwnProperty(i)) {
            temp[i] = {
                label : i,
                data : []
            }
        }
    }
}

Dzięki temu będziemy mogli łatwo wrzucać do takich obiektów odpowiednie dane.
Po utworzeniu takiej tablicy obiektów dla każdego z nich musimy ustawić „label” oraz robiąc pętlę po wszystkich zbiorach wrzucić odpowiednie dane do obiektu pod odpowiednim indeksem:

...

data.data.forEach(function(el, i) { //robimy pętlę po zbiorach
    labels.push(el.name); //w międzyczasie ustawiamy zmienną labels

    for (let i in el.values) { //dla każdego zbioru wrzucamy dane do odpowiedniego obiektu w tabeli temp
        if (el.values.hasOwnProperty(i)) {
            temp[i].data.push(el.values[i]);
        }
    }
});

...

W rezultacie tych dwuch fragmentów kodu uzyskamy dane w postaci:

labels = [...]
temp = [
    "My First dataset" : {
        label : "My First dataset",
        data : [...pogrupowane wartosci...]
    }
    "My Second dataset" : {
        label : "My Second dataset",
        data : [...pogrupowane wartosci...]
    },
    "My Third dataset" : {
        label : "My Third dataset",
        data : [...pogrupowane wartosci...]
    }
]

Jak spojrzysz na format danych jakie trafiają do ChartJS, stwierdzisz
że faktycznie labels się już zgadza, ale tablica danych data powinna być tablicą uporządkowaną (0, 1, 2), a nie
tablicą z dziwnymi nazwo-kluczami. Wystarczy użyć dodatkowej tablicy, do której takie obiekty będziemy kolejno wstawiać:

let temp2 = [];
for (let i in temp) {
    if (temp.hasOwnProperty(i)) {
        temp2.push(temp[i]);
    }
}

Cała nasza funkcja będzie miała więc postać:

let prepareData = function(data) {
    let labels = [];
    let temp = [];
    let temp2 = [];
    if (data.data.length) {
        for (let i in data.data[0].values) {
            if (data.data[0].values.hasOwnProperty(i)) {
                let color = getNextColor();
                let obConfig = setupOptions(color);
                temp[i] = $.extend({
                    label : i,
                    data : []
                }, obConfig);
            }
        }

        data.data.forEach(function(el, i) {
            labels.push(el.name);

            for (let i in el.values) {
                if (el.values.hasOwnProperty(i)) {
                    temp[i].data.push(el.values[i]);
                }
            }
        });

        for (let i in temp) {
            if (temp.hasOwnProperty(i)) {
                temp2.push(temp[i]);
            }
        }
    }

    return {
        labels : labels,
        data : temp2
    };
}

Wystarczy teraz podstawić ją pod config, który trafi do ChartJS:

$(function() {
    let prepareData = function(data) {
        ...
    }

    let getData = function() {
        ...
    }

    let init = function() {
        $.when(getData()).then(function(ret) {
            let data = prepareData(ret[0]);
            let config = {
                type : 'line',
                data : {
                    labels : data.labels,
                    datasets : data.data
                }
            }

            let $myChart = $("myChart");
            let ctx = $myChart.get(0).getContext("2d");
            myChart = new Chart(ctx, config);
        }, function(err) {
            console.warn('Błąd pobierania danych: ', err);
        });
    }
    init();
})();

Po uruchomieniu tego kodu dostaniemy ładne wykresy, ale bezbarwne, bo przecież totalnie zapomnieliśmy o konfiguracji
każdego z dataset!

Naprawmy więc i to, przy okazji rozwiązując pierwszy i trzeci punkt.
Nasza pierwotna konfiguracja bardzo się rozrastała, ponieważ robiliśmy to na sztywno. Robimy już pętlę po danych, więc przy okazji możemy też ustawiać ich wygląd i kolory.
Lekko przerabiamy funkcję prepareData:

let prepareData = function(data) {
    let labels = [];
    let temp = [];
    let temp2 = [];
    if (data.data.length) {
        for (let i in data.data[0].values) {
            if (data.data[0].values.hasOwnProperty(i)) {
                let color = getNextColor();
                let obConfig = setupOptions(color);
                temp[i] = $.extend({
                    label : i,
                    data : []
                }, obConfig);
            }
        }
    }
    ...
}

Pojedynczy obiekt w dataset ChartJS zawiera zbiór danych data, nazwę label, ale też
masę różnych konfiguracji typu borderColor. Większość z takich konfiguracji potrzebuje koloru.
Stwórzmy więc funkcję, która będzie nam takie kolory pobierać:

const colors = [
    '236,115,87',
    '252,145,58',
    '249,212,35',
    '165,210,150'
];
let colorIndex = 0;

let getNextColor = function () {
    let color = colors[colorIndex]
    colorIndex++;
    return color;
};

Czy da się powyższy kod napisać lepiej? Oczywiście. Zamiast tworzyć zewnętrzną zmienną colorIndex, możemy zastosować
sztuczkę z closure. Closure będące bardzo ważną sprawą w JS jest całkiem fajnie opisane tutaj.

const colors = [
    '236,115,87',
    '252,145,58',
    '249,212,35',
    '165,210,150'
];

let getNextColor = (function () {
    let colorIndex = 0;
    return function () {
        let color = colors[colorIndex]
        colorIndex++;
        return color;
    }
})();

Powyższa funkcja gdy zostanie wywołana, sama zadba o pobranie nowego koloru. Ale. Kolory z puli w końcu nam się skończą
a przecież datasetów możemy mieć więcej. Moglibyśmy colorIndex przywrócić do 0 i pobierać kolory od początku, ale wtedy
na wykresie mielibyśmy powtórzone kolory, co nie jest najlepszym rozwiązaniem. Albo zwiększymy liczbę kolorów w tablicy colors
do takiej liczby, że „nie ma bata”, albo po przekroczeniu ilości dostępnych kolorów będziemy je generować losowo. Wybierzmy ten
drugi sposób dodając prostą funkcję generującą losowe kolory:

const colors = [
    '255,78,80',
    '252,145,58',
    '249,212,35',
    '165,210,150'
];

function makeRandomColor() {
    let r = Math.floor(Math.random() * (255 - 0) + 0);
    let g = Math.floor(Math.random() * (255 - 0) + 0);
    let b = Math.floor(Math.random() * (255 - 0) + 0);
    return r+","+g+","+b;
}

let getNextColor = (function () {
    let colorIndex = 0;
    return function () {
        let color;
        if (colorIndex >= colors.length) {
            color = makeRandomColor();
        } else {
            color = colors[colorIndex]
        }
        colorIndex++;
        return color;
    }
})();

Skoro już mamy wygenerowany kolor, stwórzmy funkcję generującą konfigurację dla datasetów:

function setupOptions(color) {
    return {
        backgroundColor : 'rgba('+color+', 0.1)',
        borderColor : 'rgba('+color+', 1)',
        pointBorderColor : 'rgba('+color+', 1)',
        borderWidth : 2,
        //kolor tla i legendy
        fill: false, //czy wypelnic zbior
        //ustawienia punktu
        pointRadius : 4,
        pointBorderWidth : 1,
        pointBackgroundColor : 'rgba(255,255,255,1)',
        //ustawienia punktu hover
        pointHoverRadius: 4,
        pointHoverBorderWidth: 3,
        pointHoverBackgroundColor : 'rgba(255,255,255,1)',
        pointHoverBorderColor : 'rgba('+color+', 1)'
    }
}

let prepareData = function(data) {
    ...
    if (data.data.length) {
        for (let i in data.data[0].values) {
            if (data.data[0].values.hasOwnProperty(i)) {
                let color = getNextColor();
                let obConfig = setupOptions(color);
                temp[i] = $.extend({
                    label : i,
                    data : []
                }, obConfig);
            }
        }
    }
    ...
}

W funkcji init tworzymy obiekt, do którego wstawiamy powyżej wygenerowane dane. Mamy już labels, mamy też data. Brakuje nam tylko dodatkowych opcji options dla samego wykresu (takich samych jakie ustawialiśmy na samym początku tego artykułu).
Dodajmy je do obiektu config tworząc je za pomocą dodatkowej funkcji:

...

let getCharOptions = function(chartTitle) {
    return {
        animation: {
            easing: "easeOutCubic",
            duration: 700

        },
        responsive: true,
        legend: {
            position: 'bottom',
            display: true
        },
        hover: {
            //mode: 'label',
            mode: 'dataset'
        },
        scales: {
            xAxes: [{
                gridLines: {
                    zeroLineWidth: 1,
                    zeroLineColor: 'rgba(0,0,0,0.3)',
                    color: "rgba(0, 0, 0, 0.05)",
                    lineWidth: 1
                },
                display: true,
                scaleLabel: {
                    display: true,
                    labelString: 'Month',
                    fontSize: 11,
                    fontStyle: 'normal'
                },
                ticks: {
                    fontSize: 10
                },
                labelFormatter: function(e) {
                    return "x: " + e.value;
                }
            }],
            yAxes: [{
                gridLines: {
                    zeroLineWidth: 1,
                    zeroLineColor: 'rgba(0,0,0,0.3)',
                    color: "rgba(0, 0, 0, 0.05)",
                    lineWidth: 1
                },
                display: true,
                scaleLabel: {
                    display: true,
                    labelString: 'Wartość',
                    fontSize: 11,
                    fontStyle: 'normal'
                },
                ticks: {
                    fontSize: 10,
                    min: 0,
                    max: 100
                }
            }]
        },
        title: {
            display: true,
            text: chartTitle
        }
    }
}

let init = function() {
    $.when(getData()).then(function(ret) {
        let data = prepareData(ret[0]);
        let config = {
            type : 'line',
            options : getCharOptions(ret[0].chartTitle),
            data : {
                labels : data.labels,
                datasets : data.data
            }
        }

        let $myChart = $("myChart");
        let ctx = $myChart.get(0).getContext("2d");
        myChart = new Chart(ctx, config);
    }, function(err) {
        console.warn('Błąd pobierania danych: ', err);
    });
}
init();

Nasz wykres jest w zasadzie skończony. Pozostał nam do rozprawienia się ostatni krok związany z ułomnościami legendy.

Własna legenda

Poprawienie legendy zacznijmy od ukrycia domyślnej. Odpowiada za to właściwość display w powyższych opcjach wykresu. Wystarczy ustawić ją na false:

...
legend: {
    position: 'bottom',
    display: false
},
...

Wykresy ChartJS renderują to co aktualnie znajduje się w data.datasets. Jeżeli któryś ze zbiorów zostanie usunięty, wtedy nie będzie wyświetlać się na wykresie. Idąc za krokiem wystarczy więc wszystkie dane do wyświetlania gdzieś skopiować, a te które mają być aktualnie wyświetlane wstawiać lub usuwać z tej kopii w data.datasets.

Musimy więc przerobić to co do tej pory stworzyliśmy. Dodajemy do skryptu dodatkową tablicę, w której będziemy trzymać wszystkie dane, i robimy w nich kopię danych:

$(function() {
    let dataTable = [];

    ...

    let init = function() {
        $.when(getData()).then(function(ret) {
            let data = prepareData(ret[0]);
            dataTable = data.data.slice(0); //https://davidwalsh.name/javascript-clone-array
            let config = {
                type : 'line',
                options : getCharOptions(ret[0].chartTitle),
                data : {
                    labels : data.labels,
                    datasets : data.data
                }
            }
    
            let $myChart = $("myChart"); 
            let ctx = $myChart.get(0).getContext("2d");
            myChart = new Chart(ctx, config);
        }, function(err) {
            console.warn('Błąd pobierania danych: ', err);
        });
    }
    init();
});

Dane mamy teraz umieszczone w dataTable, wiec wygenerowanie własnej legendy będzie polegało na zrobieniu prostej pętli po tej tablicy i dla każdego setu wygenerowanie LI dla listy naszej legendy:

let generateOwnLegend = function() {
    let $ul = $('<ul class="chart-legend"></ul>');
    dataChart.forEach(function(el, i) {
        let $li = $('<li class="data-visible">\
                        <span class="color" style="background-color:' +el.backgroundColor+'; border-color:'+ el.borderColor+'"></span>\
                        <span class="name">' +el.label + '</span>\
                    </li>');

        //tutaj dodamy obsluge click

        $ul.append($li);
    });
    
    return $ul;
}
    
let init = function() {
    $.when(getData()).then(function(ret) {
        ...

        let $myChart = $("myChart"); 
        let ctx = $myChart.get(0).getContext("2d");
        myChart = new Chart(ctx, config);
    
        var $ownLegend = generateOwnLegend();
        $myChart.after($ownLegend);
    }, function(err) {
        ...
    });
}
init();    

Mamy wygenerowaną listę, którą wstawiliśmy za wykres. Musimy jeszcze dopiąć do niej logikę.
Domyślnie zakładamy, że każdy wykres jest wyświetlany. Określać to powinna zmienna np. display mieszcząca się w każdym zbiorze datasets. Dodajmy ją podczas generowania danych:

let prepareData = function(data) {
    let labels = [];
    let temp = [];
    let temp2 = [];
    if (data.data.length) {
        //ustawiam klucze tablicy na klucze values (temp['quaerat'])
        for (let i in data.data[0].values) {
            if (data.data[0].values.hasOwnProperty(i)) {
                let color = getNextColor();
                let obConfig = setupOptions(color);
                temp[i] = $.extend({
                    label : i,
                    data : [],
                    display: true
                }, obConfig);
            }
        }

        ...
    }

    return {
        labels : labels,
        data : temp2
    };
};

Po kliknięciu na element A legendy powinniśmy przełączać display w dataChart o danym indexie i w zależności od jej wartości kopiować dane w odpowiednie miejsce do data.datasets:

let generateOwnLegend = function() {
    let $ul = $('<ul class="chart-legend"></ul>');
    dataChart.forEach(function(el, i) {
        let $li = $('<li class="data-visible">\
                        <a href="#">\
                            <span class="color" style="background-color:' +el.backgroundColor+'; border-color:'+ el.borderColor+'"></span>\
                            <span class="name">' +el.label + '</span>\
                        </a>\
                    </li>');

        $li.children('a').on('click', function(e) {
            e.preventDefault();
            var $li = $(this).parent();
            var index = $li.index();
            if (dataChart[index].display) {
                $li.attr('data-show', 0);
                dataChart[index].display = false;
                myChart.data.datasets.splice(index, 1); //usuwamy z datasets dane o indexie index
            } else {
                $li.attr('data-show', 1);
                dataChart[index].display = true;
                myChart.data.datasets.splice(index, 1, dataChart[index]); //dodajemy do datasets w miejscu index dataChart[index]
            }
            myChart.update(); //aktualizujemy wykres
        });

        $ul.append($li);
    });

    return $ul;
};

Dodajmy także proste stylowanie dla naszej listy:

.chart-legend {
}

.chart-legend li {
    display: inline-block;
    position: relative;
    min-height: 10px;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    border: 1px solid #EEE;
    font-size: 11px;
    color: #444;
    font-family: sans-serif;
    margin-right: 5px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
    cursor: pointer;
}

.chart-legend li:hover {
    border-color: #CCC;
    box-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
}

.chart-legend li[data-show="1"] {
    opacity: 1
}

.chart-legend li[data-show="0"] {
    opacity: 0.3
}

.chart-legend a {
    display: block;
    text-decoration: none;
    color: #444;
    padding: 10px 10px 10px 22px;
}

.chart-legend .color {
    width: 10px;
    height: 10px;
    display: inline-block;
    position: absolute;
    left: 7px;
    top: 50%;
    transform: translate(0, -50%);
    border: 1px solid transparent;
}

.chart-legend .name {
    font-weight: normal;
    margin-left: 5px;
}

Dynamiczne wczytywanie danych

Pracą domową będzie pobieranie display dla datasetów z json. Dzięki temu to system będzie określał który wykres ma być domyślnie widoczny.

Własny tooltip

Na zakończenie i jako bonus w naszym skrypcie zaimplementujemy własny tooltip.
ChartJS udostępnia w opcjach właściwość tooltip, której możemy ustawić właściwość metodę custom:

...
tooltips: {
    mode: 'label',
    enabled: false,
    custom: function(tooltip) {
        // tooltip will be false if tooltip is not visible or should be hidden

        if (!tooltip) {
            return;
        } else {
            let $tooltip = $('#charTooltip.char-tooltip');

            if (tooltip.opacity) { //kursor na punkcie wykresu
                if ($tooltip.length) {

                    $tooltip.removeClass('left right'); //usuwam klasę mówiącą w którą stronę ma wskazywać tooltip
                    $tooltip.addClass(tooltip.xAlign); //dodaję tą klasę od nowa

                    let body = tooltip.body.map(function(v) { //tworzę tekst tooltipa
                        let lines = v.lines.map(function(i) {
                            return '<div>'+ i + '</div>'
                        });
                        return lines;
                    });

                    let html = '<h3 class="char-tooltip__title">' + tooltip.title + '</h3>\
                                <div class="char-tooltip__body">' + tooltip.beforeBody + body.join("\n") + '</div>';

                    $tooltip.html(html);

                    let posX = tooltip.x + 10;
                    if (tooltip.xAlign == 'right') {
                        posX = tooltip.x
                    }
                    $tooltip.css({
                        left : posX,
                        top : tooltip.y
                    })
                }
            } else {
                if ($tooltip.length) { //jak kursor nie wskazuje na punkt, przesun tooltip poza ekran
                    $tooltip.css({
                        left : -9999
                    });
                    $tooltip.html('');
                }
            }
        }
    }
},
...

Oraz proste stylowanie do tego:

.chart-tooltip {
    position: absolute;
    left: -999px;
    top: -999px;
    padding: 15px;
    background: #303E4D;
    color: #FFF;
    font-size: 12px;
    font-family: sans-serif;
    max-width: 200px;
}

.chart-tooltip .char-tooltip__title {
    font-size: 14px;
    margin: 0;
    line-height: 1;
    margin-bottom: 10px;
}

.chart-tooltip .char-tooltip__body {
    font-size: 11px;
    line-height: 1.4;
}

.chart-tooltip.left::before {
    width: 0;
    height: 0;
    border: 10px solid transparent;
    top: 5px;
    left: -20px;
    border-right-color: #303E4D;
    position: absolute;
    content: '';
}

.chart-tooltip.right::before {
    width: 0;
    height: 0;
    border: 10px solid transparent;
    top: 5px;
    right: -20px;
    border-left-color: #303E4D;
    position: absolute;
    content: '';
}

Własny tooltip

Komentarze