grofit / knockout.chart

A knockout binding to allow chart.js and knockout to work together for the greater good.
MIT License
25 stars 12 forks source link

Using this returns an empty chart #7

Closed danielriddell21 closed 3 years ago

danielriddell21 commented 5 years ago

KnockoutJS: v3.50 ChartJS: v2.80

<div id="chart-container">
    <canvas id="myChart" data-bind="chart: { type: 'line', data: chartData, options: chartOptions }"></canvas>
</div>
function ViewModel() {
        var self = this;
        self.chartData = ko.observableArray([]);
        self.chartOptions = ko.observableArray([]);

        self.refresh = function () {
            $.getJSON("/Home/GetWebSiteDetails/@Model", function (data) {
                var obj = JSON.parse(data);
                console.log(obj);

                var timestamp = [];
                var responseTimes = [];

                obj.WebsiteDetails.forEach(function(data) {
                    timestamp.push(data.Timestamp);
                    responseTimes.push(data.ResponseTime);
                });

                var randomColour = function () {
                    var r = Math.floor(Math.random() * 255);
                    var g = Math.floor(Math.random() * 255);
                    var b = Math.floor(Math.random() * 255);
                    return "rgb(" + r + "," + g + "," + b + ")";
                };
                var colour = randomColour();

                self.chartData({
                    labels: timestamp,
                    datasets: [
                        {
                            data: responseTimes,
                            label: "Response Time",
                            borderColor: colour,
                            backgroundColor: colour,
                            pointBackgroundColor: colour,
                            pointBorderColor: colour,
                            pointHoverBackgroundColor: colour,
                            pointHoverBorderColor: colour,
                            pointRadius: 4,
                            fill: false,
                            lineTension: "0.2"
                        }
                    ]
                });
                self.chartOptions({
                    responsive: true,
                    title: {
                        display: true,
                        text: obj.WebsiteDetails[0].Name,
                        fontSize: 24
                    },
                    scales: {
                        xAxes: [
                            {
                                display: true,
                                scaleLabel: {
                                    display: true,
                                    labelString: 'Date of response'
                                }
                            }
                        ],
                        yAxes: [
                            {
                                display: true,
                                scaleLabel: {
                                    display: true,
                                    labelString: 'Response Time (ms)'
                                }
                            }
                        ]
                    }
                });
            });
        }
        self.refresh();
        setInterval(self.refresh, 10000);
    }
    ko.applyBindings(app = new ViewModel());

image

image image

grofit commented 5 years ago

Hey, I don't really support this library, so it could be that chart JS has made some breaking changes, or that the data schema is just not right, hopefully someone else can chime in and help you.

danielriddell21 commented 5 years ago

Whats the best alternative for this library then, id like to comebine knockout and chart

grofit commented 5 years ago

not a clue, I made this back when there was nothing, I don't really use Knockout much these days.

The internals of this lib are not that complex so you could probably just open it up put some breakpoints and get it working or find out your issue. If you don't have time then just google for an alternative.

The library itself works with whatever versions it was developed against: https://rawgit.com/grofit/knockout.chart/master/example.html

So it may be worth going to an earlier version of chart.js

danielriddell21 commented 5 years ago

Alright cheers for your help

gregveres commented 4 years ago

@bobland1 I just included this in my project. I use typescript and it wasn't importing properly. Typescript can be finicky with importing javascript where you only want the side effects like this module.

I reworked it into typescript and it is working as advertised. It looks like you are just using javascript, so maybe what I have done doesn't help. I will post my typescript in the next response.

What I can say is that all I did when I reworked it into typescript is to move things around and reformat. The base code that grofit provided worked for me and I was able to get a chart up and running after I spent the time to learn the syntax of the chart.js options and data. At first I wasn't getting data either. My x axis was a time series and my data was Point format. I eventually got it working by making sure my x axis was specified properly.

gregveres commented 4 years ago
import { bindingHandlers, unwrap, Observable, isComputed, isObservable, toJS } from 'knockout';
import { Chart, ChartData, ChartConfiguration, ChartOptions } from 'chart.js';

class ObservableManager {
    private throttleAmt: number;
    private throttleTimeout: number;
    private observables: Observable[];

    constructor(observables: Observable[]) {
        this.throttleAmt = 0;
        this.observables = observables;
    }

    public throttle(duration: number): this {
        this.throttleAmt = duration;
        return this;
    }

    public subscribe(handler: (val: any) => void): this {
        let throttleHandler = (val: any) => {
            if (this.throttleAmt > 0) {
                if (!this.throttleTimeout) {
                    this.throttleTimeout = setTimeout(function () {
                        this.throttleTimeout = undefined;
                        handler(val);
                    }, this.throttleAmt);
                }
            }
            else { handler(val); }
        };

        for (var i = 0; i < this.observables.length; i++) {
            this.observables[i].subscribe(throttleHandler);
        }

        return this;
    }
}

// I don't know why this isn't already defined in the TS namespace since it is a builtin function of most modern browsers
declare var Reflect: any;

class KoChart {
    private observableGroup: ObservableManager;
    private activeChart: Chart;
    private chartType: string;
    private chartData: ChartData;
    private chartOptions: ChartOptions;
    private chartConfig: ChartConfiguration;
    private element: any; // from the init function of the binding
    private chartDataBindings: any; // from the init function and used for refreshing

    constructor(chartBinding: any, element: any) {
        this.element = element;

        this.chartType = unwrap(chartBinding.type);
        this.chartData = toJS(chartBinding.data);
        this.chartDataBindings = chartBinding.data; // we keep this because we need it in refresh chart
        this.chartOptions = toJS(chartBinding.options);

        this.chartConfig = {
            type: this.chartType,
            data: this.chartData,
            options: this.chartOptions
        };

        this.createChart();
        if (chartBinding.options && chartBinding.options.observeChanges) { this.subscribeToChanges(chartBinding); }
    }

    public createChart() {
        this.activeChart = new Chart(this.element, this.chartConfig);

    };

    private subscribeToChanges(chartBinding: any) {
        var throttleAmount = unwrap(chartBinding.options.throttle) || 100;
        var dataSubscribables = this.getSubscribables(chartBinding.data);

        this.observableGroup = new ObservableManager(dataSubscribables)
            .throttle(throttleAmount)
            .subscribe(() => {
                // refresh the chart
                this.chartConfig.data = toJS(this.chartDataBindings);
                this.activeChart.update();
                this.activeChart.resize();
            });
    };

    private getType(obj: any) {
        if ((obj) && (typeof (obj) === "object") && (obj.constructor == (new Date).constructor)) return "date";
        return typeof obj;
    };
    private getSubscribables(model: any): Observable[] {
        var subscribables: Observable[] = [];
        this.scanForObservablesIn(model, subscribables);
        return subscribables;
    };
    private scanForObservablesIn(model: any, subscribables: Observable[]) {
        if (model === null || model === undefined) {
            return;
        }

        var propertyNames = [];
        if (window.navigator.userAgent.indexOf("MSIE") > -1 || navigator.userAgent.indexOf("Trident") > -1) {
            propertyNames = Object.getOwnPropertyNames(model);
        }
        else {
            propertyNames = Reflect.ownKeys(model);
        }

        propertyNames.forEach((propertyName: string) => {
            var typeOfData = this.getType(model[propertyName]);
            switch (typeOfData) {
                case "object": { this.scanForObservablesIn(model[propertyName], subscribables); } break;
                case "array":
                    {
                        var underlyingArray = model[propertyName]();
                        underlyingArray.forEach((entry: any, index: number) => { this.scanForObservablesIn(underlyingArray[index], subscribables); });
                    }
                    break;

                default:
                    {
                        if (isComputed(model[propertyName]) || isObservable(model[propertyName])) { subscribables.push(model[propertyName]); }
                    }
                    break;
            }
        });
    };
}

bindingHandlers.chart = {
    init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
        var allBindings = allBindingsAccessor();
        // create the chart
        new KoChart(allBindings.chart, element);
    }
};
grofit commented 4 years ago

@gregveres feel free to do a PR with the typescript stuff in, as long as you add an npm step to build it etc I am fine with it all moving to TS. This is only in JS as I dont think TS existed when I first started writing these knockout libs.

gregveres commented 4 years ago

@bobland1 I just found out the real reason that your chart came up empty. I assume you have moved on, but I am going to put what I have learned so far: I believe that @grofit assumed that users would put all of their chart options into their html file and not in their JS file as an observable. This is why he added two values to the options that he looks for. But you and I came at it differently and we put the options into an observable. This works ok if the value of the options observable is ready and configured before the chart gets instantiated. If the options observable changes after the chart has been created, then the new version of the options is ignored.

When I used it for my first graph I was mostly getting lucky with the timing of things. I had another observable that was controlling the display of the graph and that was getting set after the options were configured.

But for my second graph, I wasn't so lucky and the data and options were getting set after the graph was already created. Then I saw that the graph was empty.

To get around this, I am making the component always react to channges in the observables used to configure the data. You do that by removing the if statement on line 133.

gregveres commented 4 years ago

oops, there was a second thing that had to happen. you have to get the options out of the options observable. After line 116 add this line: chartData.options = ko.toJS(chartBinding.options);

gregveres commented 4 years ago

ok, that didn't work either because the chart on refreshes data from the original config, it doesn't refresh options from the original config. So that line I gave above needs to become: activeChart.options = {...activeChart.options, ...ko.toJS(chartBinding.options)};