Skip to content

Instantly share code, notes, and snippets.

@jmhdez

jmhdez/ko-chart.js

Last active Feb 11, 2019
Embed
What would you like to do?
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);
@ericmasiello

This comment has been minimized.

Copy link

@ericmasiello ericmasiello commented Dec 3, 2014

Do you have an example of how this would be used within your HTML markup?

@mika76

This comment has been minimized.

Copy link

@mika76 mika76 commented Mar 7, 2015

Found small bug with latest knockout:

if (this.chart) {
this.chart.destroy();
delete this.chart;
}
this.chart = new Chart(ctx)[type](data, options);

should be replaced with

ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
$(element).chart.destroy();
delete $(element).chart;
});

$(element).chart = new Chart(ctx)[type](data, options);

also as an example:

<canvas width="200" height="200" data-bind="chartType: 'Doughnut', 
                                        chartData: Chart_ActiveMachines,
                                        chartOptions : {
                                            animation : false,
                                            showTooltips: false,
                                            percentageInnerCutout : 60,
                                            segmentShowStroke : false
                                        }
                                        "></canvas>
@jamesburton

This comment has been minimized.

Copy link

@jamesburton jamesburton commented Nov 28, 2015

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

This comment has been minimized.

Copy link

@disharide disharide commented Jun 21, 2016

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
You can’t perform that action at this time.