Skip to content

Instantly share code, notes, and snippets.

@jmhdez
Last active February 11, 2019 15:32
Show Gist options
  • Save jmhdez/4987b053e817d65d7c68 to your computer and use it in GitHub Desktop.
Save jmhdez/4987b053e817d65d7c68 to your computer and use it in GitHub Desktop.
Custom bindings to use Chart.js with Knockout.js
/*global ko, Chart */
(function(ko, Chart) {
ko.bindingHandlers.chartType = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData')) {
throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
}
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var ctx = element.getContext('2d'),
type = ko.unwrap(valueAccessor()),
data = ko.unwrap(allBindings.get('chartData')),
options = ko.unwrap(allBindings.get('chartOptions')) || {};
if (this.chart) {
this.chart.destroy();
delete this.chart;
}
this.chart = new Chart(ctx)[type](data, options);
}
};
ko.bindingHandlers.chartData = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartType')) {
throw Error('chartData must be used in conjunction with chartType and (optionally) chartOptions');
}
}
};
ko.bindingHandlers.chartOptions = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData') || !allBindings.has('chartType')) {
throw Error('chartOptions must be used in conjunction with chartType and chartData');
}
}
};
})(ko, Chart);
@jamesburton
Copy link

I have extended this to support click events on Pie and Doughnut charts (via segmentClick binding), Line charts (via lineClick), and Bar charts (via barClick), as well as the fix noted above, which you can see working here:
https://jsfiddle.net/glaivier/kywm94dv/7/

Some examples of usage are as follows:

<canvas width="200" height="200" data-bind="chartType: 'Doughnut',
segmentClick: alertLabel,

                                    chartData: machines,
                                    chartOptions : {
                                        animation : true,
                                        showTooltips: true,
                                        percentageInnerCutout : 50,
                                        segmentShowStroke : true,
                                            segmentStrokeColor: '#222222'

                                    }
                                    "></canvas>

<canvas width="300" height="150" data-bind="chartType: 'Bar',
barClick: alertJson,

                                    chartData: browserData,
                                    chartOptions : {
                                        animation : true,
                                        showTooltips: true,
                                        segmentShowStroke : true,
                                            segmentStrokeColor: '#222222'

                                    }
                                    "></canvas>

<canvas width="300" height="150" data-bind="chartType: 'Line',
lineClick: alertJson,

                                    chartData: lineData,
                                    chartOptions : {
                                        animation : true,
                                        showTooltips: true,
                                        segmentShowStroke : true,
                                            segmentStrokeColor: '#222222'"></canvas>

..... Use this with models such as these:

function KoModel(data) {
var self = this;
self.machines = ko.observableArray(data.machines);
self.browserData = ko.observable(data.browsers || {});
self.lineData = ko.observable(data.lineData || {});
self.alertLabel = function (chartItem, chart) {
alert(chartItem.label);
};
self.alertJson = function (chartItem, chart) {
alert(JSON.stringify(chartItem));
};
};

$(document).ready(function () {
var data = {
machines: [{
value: 30,
color: '#FF3333',
highlight: '#FF7777',
label: 'PC'
}, {
value: 85,
color: '#3333FF',
highlight: '#7777FF',
label: 'Android'
}, {
value: 15,
color: '#33FF33',
highlight: '#77FF77',
label: 'Linux'
}],
browserData: {
labels: ["September", "October", "November", "Dcember"],
datasets: [{
label: 'IE',
fillColor: 'rgba(250, 50, 50, 0.5)',
strokeColor: 'rgba(250, 50, 50, 0.8)',
highlightFill: 'rgba(250, 50, 50, 0.75)',
highlightStroke: 'rgba(250, 50, 50, 1)',
data: [60, 50, 45, 32]
}, {
label: 'Firefox',
fillColor: 'rgba(50, 50, 250, 0.5)',
strokeColor: 'rgba(50, 50, 250, 0.8)',
highlightFill: 'rgba(50, 50, 250, 0.75)',
highlightStroke: 'rgba(50, 50, 250, 1)',
data: [25, 22, 15, 24]
}, {
label: 'Chrome',
fillColor: 'rgba(50, 250, 50, 0.5)',
strokeColor: 'rgba(50, 250, 50, 0.8)',
highlightFill: 'rgba(50, 250, 50, 0.75)',
highlightstroke: 'rgba(50, 250, 50, 1)',
data: [10, 12, 16, 30]
}]
},
lineData: {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [{
label: "My First dataset",
fillColor: "rgba(220,120,220,0.2)",
strokeColor: "rgba(220,120,220,1)",
pointColor: "rgba(220,120,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [75, 69, 90, 91, 66, 65, 50]
}, {
label: "My Second dataset",
fillColor: "rgba(251,87,105,0.2)",
strokeColor: "rgba(251,87,105,1)",
pointColor: "rgba(251,87,105,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [38, 78, 20, 69, 26, 47, 10]
}, {
label: "Thurd dataset",
fillColor: "rgba(45,217,125,0.2)",
strokeColor: "rgba(45,217,125,1)",
pointColor: "rgba(45,217,125,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [18, 28, 50, 32, 66, 57, 86]
}]
}
};
var koModel = new KoModel(data);
ko.applyBindings(koModel);
});

The full JS file is as follows:

/*global ko, Chart */
// knockout-chartjs binding by James Burton (extended from example by jmhdez on GitHubGist to include fix and click events)
// Example: https://jsfiddle.net/glaivier/kywm94dv/7/

(function (ko, Chart) {
ko.bindingHandlers.barClick = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData')) {
throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
return;
}
var chartType = allBindings.get('chartType');
if (chartType !== 'Bar') {
throw Error('barClick can only be used with chartType Bar');
return;
}
},
update: function (element, valueAccesor, allBindings, viewModel, bindingContext) { }
};
ko.bindingHandlers.lineClick = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData')) {
throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
return;
}
var chartType = allBindings.get('chartType');
if (chartType !== 'Line') {
throw Error('lineClick can only be used with chartType Line');
return;
}
},
update: function (element, valueAccesor, allBindings, viewModel, bindingContext) { }
};
ko.bindingHandlers.segmentClick = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData')) {
throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
return;
}
var chartType = allBindings.get('chartType');
if (chartType !== 'Pie' && chartType !== 'Doughnut') {
throw Error('segmentClick can only be used with chartType Pie or Donut');
return;
}
},
update: function (element, valueAccesor, allBindings, viewModel, bindingContext) { }
};
ko.bindingHandlers.chartType = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!allBindings.has('chartData')) {
throw Error('chartType must be used in conjunction with chartData and (optionally) chartOptions');
}
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var ctx = element.getContext('2d'),
type = ko.unwrap(valueAccessor()),
data = ko.unwrap(allBindings.get('chartData')),
options = ko.unwrap(allBindings.get('chartOptions')) || {},
segmentClick = ko.unwrap(allBindings.get('segmentClick')),
barClick = ko.unwrap(allBindings.get('barClick')),
lineClick = ko.unwrap(allBindings.get('lineClick'));

        /* NB: Fix for newer knockout (see https://gist.github.com/jmhdez/4987b053e817d65d7c68)
        if (this.chart) {
            this.chart.destroy();
            delete this.chart;
        }

        this.chart = new Chart(ctx)[type](data, options);
        //*/

        ko.utils.domNodeDisposal.addDisposeCallback(element,

        function () {
            $(element).chart.destroy();
            delete $(element).chart;
        });

        var newChart = new Chart(ctx)[type](data, options);
        var $element = $(element)[0];
        $element.chart = newChart;
        //* End of fix

        //* Remove existing click binding
        if ($element.click) {
            $element.removeEventListener('click', $element.click);
            delete ($element.click);
        }
        //* Add segment click binding
        switch (type) {
            case "Pie":
            case "Doughnut":
                if (segmentClick) {
                    $element.click = function (evt) {
                        var activePoints = newChart.getSegmentsAtEvent(evt);
                        segmentClick(activePoints[0], newChart);
                    };
                }
                break;
            case "Bar":
                if (barClick) {
                    $element.click = function (evt) {
                        barClick(newChart.getBarsAtEvent(evt), newChart);
                    };
                }
                break;
            case "Line":
                if (lineClick) {
                    $element.click = function (evt) {
                        lineClick(newChart.getPointsAtEvent(evt), newChart);
                    };
                }
                break;
            default:
                break;
        }
        $element.addEventListener('click', $element.click);
    }
};

ko.bindingHandlers.chartData = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        if (!allBindings.has('chartType')) {
            throw Error('chartData must be used in conjunction with chartType and (optionally) chartOptions');
        }
    }
};

ko.bindingHandlers.chartOptions = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        if (!allBindings.has('chartData') || !allBindings.has('chartType')) {
            throw Error('chartOptions must be used in conjunction with chartType and chartData');
        }
    }
};

})(ko, Chart);

@disharide
Copy link

Any option of using the same without defining the colours inside the datasets? I am intending to fetch data from back end services. So Defining colours at that level is a bit tricky because I won't have any idea about the dataset's size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment