简体   繁体   中英

dynamically update the scale type of chartjs graph

I have a bar chart on a page that has data displayed that is dynamically updated. The problem I have is that I need to change the type of the y-axis based on the data that is displayed. I need it to be a linear scale when the data has a range of less than 100 (eg 0-99) and logarithmic if it is greater (eg 0-1000).

I can calculate whether I need a logarithmic scale or not fine, but when it comes to changing the scale type I am at a loss. I have tried changing the options object of the chart and calling update() :

myChart.options/scales.yAxes.map(function (axis) { axis.type = 'logarithmic' });
myChart.update();

and also tried changing the scale itself directly:

Object.keys(mychart.scales).forEach((function (axisName) {
            var axis = myChart.scales[axisName];
            if (axis && axis.id.startsWith('y')) {
                axis.options.type = 'logarithmic'
            }
        }));
myChart.update();

then I tried using the beforeBuildTicks callback, thinking that changing the type while updating the data and redrawing the graph maybe too late or just at the wrong time:

beforeBuildTicks: function (scale) {
                                if (scale.chart) {
                                    console.log('logarithmic');
                                    scale.options.type = 'logarithmic';
                                 }
                            }

all of these approaches change the chart object, but the display is not affected.

Is there a way to dynamically change the scale type of a chartjs bar chart without destroying and recreating the entire chart?

I finally found a solution to this, but it isn't straightforward.

Thanks to @jordanwillis for the pointer in the right direction in terms of using the scaleMerge() helper function.

I created a plugin that does all the work:

var changeScaleTypePlugin = {
    beforeUpdate: function(chartInstance) {
        var self = this;
        chartInstance.beforeInit = null;
        if (chartInstance.options.changeScale) {
            self.changeScales(chartInstance);
            if (chartInstance.options.scaleTypeChanged) {
                chartInstance.options.scaleTypeChanged = false;
                Object.keys(chartInstance.scales).forEach(function(axisName) {
                    var scale = chartInstance.scales[axisName];
                    Chart.layoutService.removeBox(chartInstance, scale);
                });
                chartInstance.initialize();
            }
        }
    },
    changeScales: function(chartInstance) {
        var maxValue = Math.max.apply(null, chartInstance.data.datasets.map(function(dataset) {
            return Math.max.apply(null, dataset.data);
        }));
        var minValue = Math.min.apply(null, chartInstance.data.datasets.map(function(dataset) {
            return Math.min.apply(null, dataset.data);
        }));
        var logMax = Math.floor(Math.log(maxValue) / Math.LN10);
        var logMin = Math.floor(Math.log(minValue) / Math.LN10);
        if (logMax - logMin > chartInstance.options.maxRankDifference) {
            if (!chartInstance.options.scaleTypeChanged && chartInstance.options.scales.yAxes.filter(function(axis) {
                    return axis.type !== 'logarithmic';
                }).length) {
                console.log('logarithmic');
                chartInstance.options.scaleTypeChanged = true;
                chartInstance.options.scales.yAxes = Chart.helpers.scaleMerge(Chart.defaults.scale, {
                    yAxes: chartInstance.options.logarithmicScaleOptions
                }).yAxes;
            }
        } else {
            if (!chartInstance.options.scaleTypeChanged && chartInstance.options.scales.yAxes.filter(function(axis) {
                    return axis.type !== 'linear';
                }).length) {
                console.log('linear');
                chartInstance.options.scaleTypeChanged = true;
                chartInstance.options.scales.yAxes = Chart.helpers.scaleMerge(Chart.defaults.scale, {
                    yAxes: chartInstance.options.linearScaleOptions
                }).yAxes;
            }
        }
    }
};

Chart.pluginService.register(changeScaleTypePlugin);

To apply this simply add a few properties to the options of the chart:

options: {
    linearScaleOptions: [{
        id: 'y-axis-0',
        type: 'linear',
        tick: {
            // callback: Chart.Ticks.formatters.linear,
            min: 0
        }
    }],
    logarithmicScaleOptions: [{
        id: 'y-axis-0',
        type: 'logarithmic',
        ticks: {
            // callback: function (tickValue, index, ticks)  {
            //        var remain = tickValue / (Math.pow(10, Math.floor(Math.log(tickValue) / Math.LN10)));

            //        if (tickValue === 0) {
            //            return '0';
            //        } else if (remain === 1 || remain === 2 || remain === 5 || index === 0 || index === ticks.length - 1) {
            //            return tickValue;
            //        }
            //        return '';
            //},
            min: 0
        }
    }],
    changeScale: true,
    maxRankDifference: 1,
}

The commented out callback on the ticks is for the situation where (like me) you do not want the tick values shown in scientific notation on the log scale.

This gives the output that looks like:

对数的

with the max rank property set to 1

or

线性

with it set to 3

or with the callback set:

自然刻度的对数

You were on the right track, but the problem is for each scale type, there is a corresponding set of properties that are used by the underlying scale service to render the scale.

So when you configure your scale to be 'linear', that triggers a set of hidden axis properties to be set that produce the rendered linear scale. However, when you configure your scale to be 'logarithmic', the hidden axis properties need to be set differently to produce the rendered logarithmic scale.

Long story short, you must use the Chart.helpers.scaleMerge() function to handle all this for you (you can't just simple change the scale type in the chart options and re-render).

I created a working example demonstrating how to achieve what you are after. The essence of how this works is displayed below.

document.getElementById('changeScaleAndData').addEventListener('click', function() {
  if (isLinear) {
    myBar.options.scales.yAxes = Chart.helpers.scaleMerge(Chart.defaults.scale, {yAxes: logarithmicScaleOptions}).yAxes;
    isLinear = false;
  } else {
    myBar.options.scales.yAxes = Chart.helpers.scaleMerge(Chart.defaults.scale, {yAxes: linearScaleOptions}).yAxes;
    isLinear = true;
  }

  myBar.data.datasets[0].data = [
    randomScalingFactor(), 
    randomScalingFactor(), 
    randomScalingFactor(), 
  ];

  myBar.update();
});

I find it easier to delete and recreate the chart after changing the axis type. IE if I have a checkbox with the id logScale to set whether my axis is log, I can just say:

$("#logScale").change(function () {
    $('#chart').remove();
    createChart();
})

where createChart is the code to create the chart. Note that I reference #logScale to set the Y axis type:

$('#chartContainer').append('<canvas id="chart" style="max-height:300px;max-width:95%"></canvas>');
    var axisType = $("#logScale").prop("checked") ? 'logarithmic' : 'linear';
        ctx = document.getElementById('chart').getContext('2d');
        chartObject = new Chart(ctx, {
            options: {
                scales: {
                    xAxes: [{
                    yAxes: [{
                        type: axisType
                    }]
                } //and whatever other chart data you have

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM